openapi: 3.0.0
info:
  title: bc-subscriptions API
  version: 1.0.0
  description: BigCommerce-native subscription management API. Admin routes
    require a Bearer JWT from /api/load. Portal routes require a portal_session
    cookie from /api/portal/auth/verify.
  contact:
    name: bc-subscriptions team
servers:
  - url: https://api.bcsubs.dev
    description: Production (Cloudflare Worker)
  - url: http://localhost:8787
    description: Local development (wrangler dev)
tags:
  - name: plans
    description: Subscription plan management
  - name: subscriptions
    description: Subscription lifecycle
  - name: charges
    description: Charge records and retry
  - name: portal
    description: Subscriber self-service portal
  - name: auth
    description: Authentication flows
  - name: admin
    description: Merchant-admin and CS-tools operations
  - name: storefront
    description: Public storefront plan discovery
  - name: dashboard
    description: Merchant dashboard aggregates
  - name: eligibility
    description: Product and category eligibility rules
  - name: catalog
    description: BC catalog proxy
  - name: onboarding
    description: Setup checklist and health
  - name: reconciliation
    description: Charge reconciliation
  - name: exceptions
    description: Failed charge exceptions queue
  - name: compliance
    description: DSAR and data compliance
  - name: b2b
    description: B2B enrollment
  - name: webhooks
    description: Inbound BC webhooks
  - name: install
    description: BC App install/uninstall callbacks
  - name: audit
    description: Audit event logs
  - name: internal
    description: Internal health-check endpoints
  - name: health
    description: Registration health and re-registration
  - name: cs-tools
    description: Customer-service override tools
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: BigCommerce signed-payload JWT (from /api/load callback)
    PortalSessionCookie:
      type: apiKey
      in: cookie
      name: portal_session
      description: Portal session JWT set by /api/portal/auth/verify
  schemas:
    ErrorEnvelope:
      type: object
      properties:
        kind:
          type: string
          enum:
            - resource_not_found
            - invalid_fields
            - invalid_body
            - conflict
            - forbidden
            - unauthenticated
            - rate_limited
            - upstream_failure
            - precondition_failed
          description: Machine-readable error discriminant
        message:
          type: string
          description: Human-readable detail (optional)
      required:
        - kind
    Plan:
      type: object
      properties:
        id:
          type: string
          format: uuid
        store_hash:
          type: string
        name:
          type: string
        amount_cents:
          type: integer
        currency:
          type: string
          minLength: 3
          maxLength: 3
        interval:
          type: string
          enum:
            - day
            - week
            - month
            - year
        interval_count:
          type: integer
          minimum: 1
        status:
          type: string
          enum:
            - active
            - archived
        accepting_new_subscribers:
          type: boolean
        bc_product_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
      required:
        - id
        - store_hash
        - name
        - amount_cents
        - currency
        - interval
        - interval_count
        - status
        - accepting_new_subscribers
        - bc_product_id
        - created_at
    Subscription:
      type: object
      properties:
        id:
          type: string
          format: uuid
        store_hash:
          type: string
        customer_id:
          type: string
          format: uuid
        plan_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - active
            - paused
            - past_due
            - cancelled
            - trialing
            - pending_start
            - on_hold_oos
          description: Lifecycle status of a subscription
        next_charge_at:
          type: string
          nullable: true
          format: date-time
        created_at:
          type: string
          format: date-time
        cancelled_at:
          type: string
          nullable: true
          format: date-time
        metadata:
          type: object
          nullable: true
          additionalProperties:
            nullable: true
      required:
        - id
        - store_hash
        - customer_id
        - plan_id
        - status
        - next_charge_at
        - created_at
        - cancelled_at
        - metadata
    Charge:
      type: object
      properties:
        id:
          type: string
          format: uuid
        subscription_id:
          type: string
          format: uuid
        store_hash:
          type: string
        amount_cents:
          type: integer
        currency:
          type: string
          minLength: 3
          maxLength: 3
        status:
          type: string
          enum:
            - pending
            - processing
            - succeeded
            - failed
            - failed_permanently
            - refunded
            - requires_action
            - abandoned
        scheduled_at:
          type: string
          format: date-time
        succeeded_at:
          type: string
          nullable: true
          format: date-time
        bc_order_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
      required:
        - id
        - subscription_id
        - store_hash
        - amount_cents
        - currency
        - status
        - scheduled_at
        - succeeded_at
        - bc_order_id
        - created_at
    PortalAddress:
      type: object
      properties:
        first_name:
          type: string
        last_name:
          type: string
        street_1:
          type: string
        street_2:
          type: string
        city:
          type: string
        state_or_province:
          type: string
        country_iso2:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2 country code (e.g. "US")
        postal_code:
          type: string
        phone:
          type: string
      required:
        - first_name
        - last_name
        - street_1
        - city
        - state_or_province
        - country_iso2
        - postal_code
  parameters: {}
paths:
  /openapi.json:
    get:
      tags:
        - internal
      summary: OpenAPI 3.0 spec as JSON (BRD §US-27.1)
      responses:
        "200":
          description: OpenAPI 3.0 spec document
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  nullable: true
  /api/openapi.yaml:
    get:
      tags:
        - internal
      summary: OpenAPI 3.0 spec as YAML
      responses:
        "200":
          description: OpenAPI 3.0 spec document (YAML)
          content:
            text/yaml:
              schema:
                type: string
  /health:
    get:
      tags:
        - internal
      summary: Service health check
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  service:
                    type: string
                required:
                  - ok
                  - service
  /api/auth:
    get:
      tags:
        - install
      summary: OAuth install callback from BigCommerce
      parameters:
        - schema:
            type: string
          required: true
          name: code
          in: query
        - schema:
            type: string
          required: true
          name: scope
          in: query
        - schema:
            type: string
          required: true
          name: context
          in: query
      responses:
        "302":
          description: Redirects to admin app after successful install
        "400":
          description: Missing or invalid OAuth parameters
  /api/load:
    get:
      tags:
        - install
      summary: App load callback — verifies signed-payload JWT and redirects to admin
        SPA
      parameters:
        - schema:
            type: string
          required: true
          name: signed_payload_jwt
          in: query
      responses:
        "302":
          description: Redirects to admin SPA with JWT in query string
        "401":
          description: Invalid or expired JWT
  /api/uninstall:
    get:
      tags:
        - install
      summary: Uninstall callback from BigCommerce — starts data retention window
      responses:
        "200":
          description: Acknowledged
  /webhooks/bc:
    post:
      tags:
        - webhooks
      summary: BigCommerce inbound webhook receiver
      parameters:
        - schema:
            type: string
          required: true
          name: x-bc-webhook-secret
          in: header
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                scope:
                  type: string
                store_id:
                  type: string
                data:
                  type: object
                  additionalProperties:
                    nullable: true
                created_at:
                  type: integer
              required:
                - scope
                - store_id
                - data
                - created_at
      responses:
        "200":
          description: Processed
        "401":
          description: Invalid signature
  /api/v1/storefront/plans:
    get:
      tags:
        - storefront
      summary: Discover active subscription plans for a product (storefront PDP widget)
      parameters:
        - schema:
            type: string
          required: true
          name: store_hash
          in: query
        - schema:
            type: integer
            nullable: true
          required: false
          name: bc_product_id
          in: query
      responses:
        "200":
          description: Active plans for the product
          content:
            application/json:
              schema:
                type: object
                properties:
                  plans:
                    type: array
                    items:
                      $ref: "#/components/schemas/Plan"
                required:
                  - plans
        "400":
          description: Missing required query params
  /api/v1/storefront/coupons/visible:
    get:
      tags:
        - storefront
      summary: List subscription-eligible coupon promotion ids visible on the storefront
      parameters:
        - schema:
            type: string
          required: true
          name: X-Store-Hash
          in: header
      responses:
        "200":
          description: Visible promotion ids + whether the cart context has a subscription
            item
          content:
            application/json:
              schema:
                type: object
                properties:
                  visible_bc_promotion_ids:
                    type: array
                    items:
                      type: integer
                  has_subscription:
                    type: boolean
                required:
                  - visible_bc_promotion_ids
                  - has_subscription
        "400":
          description: Missing X-Store-Hash header
  /api/v1/storefront/subscription-promotions/validate-coupon:
    post:
      tags:
        - storefront
      summary: Validate a coupon code for the storefront cart (subscription-aware)
      parameters:
        - schema:
            type: string
          required: true
          name: X-Store-Hash
          in: header
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: string
                  description: Coupon code to validate
                has_subscription_item:
                  type: boolean
                  description: Whether the cart contains a subscription line item
              required:
                - code
      responses:
        "200":
          description: Uniform validity envelope. `valid:false` with a `reason` is
            returned for unknown / expired / ineligible codes (no code
            enumeration).
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  reason:
                    type: string
                    description: Failure reason when valid=false (e.g. unknown_code,
                      subscription_only)
                required:
                  - valid
                additionalProperties:
                  nullable: true
        "400":
          description: Missing X-Store-Hash header, malformed body, or missing code
  /api/v1/storefront/orders/{orderId}/attach-subscription:
    post:
      tags:
        - storefront
      summary: "Post-purchase fallback: attach a subscription to an existing order
        (US-9.3)"
      parameters:
        - schema:
            type: string
            description: BC order id
          required: true
          description: BC order id
          name: orderId
          in: path
        - schema:
            type: string
          required: true
          name: store_hash
          in: query
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                sub_plan_id:
                  type: string
                  format: uuid
                sub_quantity:
                  type: integer
                  description: Defaults to 1
              required:
                - sub_plan_id
      responses:
        "200":
          description: Subscription attached (idempotent if one already exists for the
            order)
        "400":
          description: Missing store_hash, invalid order id, or missing sub_plan_id
        "403":
          description: Store has no stored access token
        "404":
          description: Order not found in BC
        "502":
          description: Failed to verify the order with BigCommerce
  /api/v1/storefront/cart/{cartId}/intents:
    get:
      tags:
        - storefront
      summary: List subscription intents for a cart
      parameters:
        - schema:
            type: string
          required: true
          name: cartId
          in: path
        - schema:
            type: string
          required: true
          name: store_hash
          in: query
      responses:
        "200":
          description: Current cart intents map
          content:
            application/json:
              schema:
                type: object
                properties:
                  cart_id:
                    type: string
                  intents:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        plan_id:
                          type: string
                          format: uuid
                        cadence:
                          type: string
                        interval_count:
                          type: integer
                      required:
                        - plan_id
                        - cadence
                        - interval_count
                      description: A single cart line subscription intent
                    description: Map of cart lineEntityId → subscription intent
                required:
                  - cart_id
                  - intents
        "400":
          description: Missing store_hash query parameter
        "502":
          description: BC cart-metafields upstream failure
    post:
      tags:
        - storefront
      summary: Add a subscription intent to a cart line
      parameters:
        - schema:
            type: string
          required: true
          name: cartId
          in: path
        - schema:
            type: string
          required: true
          name: store_hash
          in: query
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                plan_id:
                  type: string
                  format: uuid
                cadence:
                  type: string
                interval_count:
                  type: integer
                lineEntityId:
                  type: string
                  description: Cart line entity id to key the intent under
              required:
                - plan_id
                - cadence
                - interval_count
                - lineEntityId
              description: A single cart line subscription intent
      responses:
        "201":
          description: Intent added; returns the updated map
          content:
            application/json:
              schema:
                type: object
                properties:
                  cart_id:
                    type: string
                  intents:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        plan_id:
                          type: string
                          format: uuid
                        cadence:
                          type: string
                        interval_count:
                          type: integer
                      required:
                        - plan_id
                        - cadence
                        - interval_count
                      description: A single cart line subscription intent
                    description: Map of cart lineEntityId → subscription intent
                required:
                  - cart_id
                  - intents
        "400":
          description: Missing store_hash, malformed body, or missing lineEntityId
        "409":
          description: Concurrent write conflict (CAS retries exhausted)
        "502":
          description: BC cart-metafields upstream failure
  /api/v1/storefront/cart/{cartId}/intents/{lineEntityId}:
    put:
      tags:
        - storefront
      summary: Update a single cart line subscription intent
      parameters:
        - schema:
            type: string
          required: true
          name: cartId
          in: path
        - schema:
            type: string
          required: true
          name: lineEntityId
          in: path
        - schema:
            type: string
          required: true
          name: store_hash
          in: query
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                plan_id:
                  type: string
                  format: uuid
                cadence:
                  type: string
                interval_count:
                  type: integer
              required:
                - plan_id
                - cadence
                - interval_count
              description: A single cart line subscription intent
      responses:
        "200":
          description: Intent updated; returns the updated map
          content:
            application/json:
              schema:
                type: object
                properties:
                  cart_id:
                    type: string
                  intents:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        plan_id:
                          type: string
                          format: uuid
                        cadence:
                          type: string
                        interval_count:
                          type: integer
                      required:
                        - plan_id
                        - cadence
                        - interval_count
                      description: A single cart line subscription intent
                    description: Map of cart lineEntityId → subscription intent
                required:
                  - cart_id
                  - intents
        "400":
          description: Missing store_hash or malformed body
        "404":
          description: No intent exists for that lineEntityId
        "409":
          description: Concurrent write conflict (CAS retries exhausted)
        "502":
          description: BC cart-metafields upstream failure
    delete:
      tags:
        - storefront
      summary: Remove a single cart line subscription intent
      parameters:
        - schema:
            type: string
          required: true
          name: cartId
          in: path
        - schema:
            type: string
          required: true
          name: lineEntityId
          in: path
        - schema:
            type: string
          required: true
          name: store_hash
          in: query
      responses:
        "200":
          description: Intent removed; returns the updated map
          content:
            application/json:
              schema:
                type: object
                properties:
                  cart_id:
                    type: string
                  intents:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        plan_id:
                          type: string
                          format: uuid
                        cadence:
                          type: string
                        interval_count:
                          type: integer
                      required:
                        - plan_id
                        - cadence
                        - interval_count
                      description: A single cart line subscription intent
                    description: Map of cart lineEntityId → subscription intent
                required:
                  - cart_id
                  - intents
        "400":
          description: Missing store_hash query parameter
        "404":
          description: No intent exists for that lineEntityId
        "502":
          description: BC cart-metafields upstream failure
  /api/v1/plans:
    get:
      tags:
        - plans
      summary: List subscription plans
      security: &a1
        - BearerAuth: []
      responses:
        "200":
          description: Plan list
          content:
            application/json:
              schema:
                type: object
                properties:
                  plans:
                    type: array
                    items:
                      $ref: "#/components/schemas/Plan"
                required:
                  - plans
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - plans
      summary: Create a subscription plan
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                amount_cents:
                  type: integer
                currency:
                  type: string
                  minLength: 3
                  maxLength: 3
                interval:
                  type: string
                  enum:
                    - day
                    - week
                    - month
                    - year
                interval_count:
                  type: integer
                  minimum: 1
                bc_product_id:
                  type: integer
                  nullable: true
              required:
                - name
                - amount_cents
                - currency
                - interval
                - interval_count
      responses:
        "201":
          description: Created plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Plan"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/plans/{id}:
    get:
      tags:
        - plans
      summary: Get a plan by ID
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Plan"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - plans
      summary: Update a plan
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                amount_cents:
                  type: integer
                interval:
                  type: string
                  enum:
                    - day
                    - week
                    - month
                    - year
                interval_count:
                  type: integer
                  minimum: 1
      responses:
        "200":
          description: Updated plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Plan"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    delete:
      tags:
        - plans
      summary: Archive (soft-delete) a plan
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Archived
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/plans/{id}/pause-new-signups:
    post:
      tags:
        - plans
      summary: Pause new signups for a plan
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/plans/{id}/resume-new-signups:
    post:
      tags:
        - plans
      summary: Resume new signups for a plan
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/subscriptions:
    get:
      tags:
        - subscriptions
        - admin
      summary: List subscriptions (admin, paginated)
      security: *a1
      parameters:
        - schema:
            type: string
          required: false
          name: status
          in: query
        - schema:
            type: string
          required: false
          name: q
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: plan_id
          in: query
        - schema:
            type: integer
            minimum: 1
            default: 1
          required: false
          name: page
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          required: false
          name: page_size
          in: query
      responses:
        "200":
          description: Subscription list
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Subscription"
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      page:
                        type: integer
                      page_size:
                        type: integer
                    required:
                      - total
                      - page
                      - page_size
                required:
                  - subscriptions
                  - pagination
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - subscriptions
        - admin
      summary: Create a subscription via checkout flow
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                plan_id:
                  type: string
                  format: uuid
                customer_bc_id:
                  type: integer
                start_date:
                  type: string
                  format: date-time
              required:
                - plan_id
                - customer_bc_id
      responses:
        "201":
          description: Created subscription
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/subscriptions/{id}:
    get:
      tags:
        - subscriptions
        - admin
      summary: Get subscription detail (with charges + customer + plan)
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Subscription detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription:
                    $ref: "#/components/schemas/Subscription"
                  customer:
                    type: object
                    properties:
                      id:
                        type: string
                      bc_customer_id:
                        type: number
                      email:
                        type: string
                    required:
                      - id
                      - bc_customer_id
                      - email
                  plan:
                    $ref: "#/components/schemas/Plan"
                  charges:
                    type: object
                    properties:
                      scheduled:
                        type: array
                        items:
                          $ref: "#/components/schemas/Charge"
                      completed:
                        type: array
                        items:
                          $ref: "#/components/schemas/Charge"
                      failed:
                        type: array
                        items:
                          $ref: "#/components/schemas/Charge"
                    required:
                      - scheduled
                      - completed
                      - failed
                  latest_bc_order_id:
                    type: integer
                    nullable: true
                required:
                  - subscription
                  - customer
                  - plan
                  - charges
                  - latest_bc_order_id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/create:
    post:
      tags:
        - subscriptions
        - admin
        - cs-tools
      summary: Manually create a subscription (CS / merchant)
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_email:
                  type: string
                  format: email
                plan_id:
                  type: string
                  format: uuid
                start_date:
                  type: string
                  format: date-time
                charge_immediately:
                  type: boolean
              required:
                - customer_email
                - plan_id
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription_id:
                    type: string
                    format: uuid
                required:
                  - subscription_id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/{id}/events:
    get:
      tags:
        - subscriptions
        - admin
        - audit
      summary: Get event log for a subscription
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Event log
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        type:
                          type: string
                        actor_kind:
                          type: string
                          enum:
                            - user
                            - system
                            - customer
                        payload:
                          type: object
                          nullable: true
                          additionalProperties:
                            nullable: true
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - type
                        - actor_kind
                        - payload
                        - created_at
                required:
                  - events
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/{id}/note:
    put:
      tags:
        - subscriptions
        - admin
        - cs-tools
      summary: Set or clear merchant note on a subscription
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  nullable: true
              required:
                - note
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/subscriptions/{id}/instances/{instance_id}:
    patch:
      tags:
        - subscriptions
        - admin
      summary: Update a subscription delivery instance
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: instance_id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                quantity:
                  type: integer
                  minimum: 1
                variant_id:
                  type: integer
      responses:
        "200":
          description: Updated instance
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/{id}/force-cancel:
    post:
      tags:
        - subscriptions
        - admin
        - cs-tools
      summary: Force-cancel a subscription
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/{id}/force-retry:
    post:
      tags:
        - subscriptions
        - admin
        - cs-tools
      summary: Force-retry the next charge for a subscription
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/bulk:
    post:
      tags:
        - subscriptions
        - admin
      summary: Apply a bulk action across many subscriptions
      description: Epic-21 US-21.5. Applies a single action (skip | pause | cancel) to
        up to 500 subscriptions in one call. Processed per-id (no transaction
        wrap) so one bad row does not poison the batch — the response reports
        per-id success/failure. Out-of-tenant or missing ids surface as
        `not_found` in `failed[]`; already-cancelled rows as
        `already_cancelled`.
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subscription_ids:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 500
                action:
                  type: string
                  enum:
                    - skip
                    - pause
                    - cancel
                reason:
                  type: string
                  maxLength: 200
              required:
                - subscription_ids
                - action
      responses:
        "200":
          description: Per-id outcome
          content:
            application/json:
              schema:
                type: object
                properties:
                  succeeded:
                    type: array
                    items:
                      type: string
                  failed:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        reason:
                          type: string
                      required:
                        - id
                        - reason
                required:
                  - succeeded
                  - failed
        "400":
          description: Invalid body (not JSON / not an object)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (subscription_ids / action / reason)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/export:
    get:
      tags:
        - subscriptions
        - admin
      summary: Export filtered subscriptions as CSV
      description: "Epic-21 US-21.7. Streams tenant-scoped subscription rows as CSV
        using the same filters as the list view (status, q, plan_id). Phase-1
        ships synchronous streaming (no async job). Columns: subscription_id,
        customer_name, customer_email, plan_name, status, next_charge_at,
        last_charge_at, total_charges_count, mrr_cents. Capped at 50,000 rows."
      security: *a1
      parameters:
        - schema:
            type: string
            enum:
              - csv
            description: only 'csv' is supported (default csv)
          required: false
          description: only 'csv' is supported (default csv)
          name: format
          in: query
        - schema:
            type: string
          required: false
          name: status
          in: query
        - schema:
            type: string
          required: false
          name: q
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: plan_id
          in: query
      responses:
        "200":
          description: CSV stream (text/csv; attachment)
          content:
            text/csv:
              schema:
                type: string
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (unsupported format)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/{id}/cancel:
    post:
      tags:
        - subscriptions
        - admin
        - cs-tools
      summary: Cancel a subscription on behalf of the subscriber
      description: Epic-18 US-18.5. Merchant-actor cancel that records
        cancel_reason=cancelled_on_behalf_by_merchant and emits
        subscription.cancelled_on_behalf — distinct from the force-cancel
        override. No request body. Returns 409 if already cancelled.
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/{id}/charges/{charge_id}/retry:
    post:
      tags:
        - subscriptions
        - charges
        - admin
        - cs-tools
      summary: Retry a specific charge by ID
      description: Clones the target charge into a new pending charge with
        retry_attempt incremented; the scheduler picks it up on the next tick
        (no synchronous processor call). Emits charge.retry_queued. Returns 404
        if the charge does not belong to this subscription/store, 409 if the
        charge is not in a retryable state (failed | requires_action).
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: charge_id
          in: path
      responses:
        "200":
          description: Retry queued (new charge id)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  charge_id:
                    type: string
                    format: uuid
                required:
                  - ok
                  - charge_id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Charge not retryable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscriptions/{id}/manual-charge:
    post:
      tags:
        - subscriptions
        - charges
        - admin
      summary: Run an ad-hoc manual charge against a subscription
      description: Epic-12 US-12.4. Merchant-initiated immediate charge against the
        customer’s default stored payment method (MIT type "unscheduled"). A
        successful charge and a processor decline BOTH return HTTP 200 —
        distinguish via the `ok` flag (decline carries ok=false plus
        failure_code / failure_message). Returns 403 when the subscriber has not
        opted in to unscheduled charges, 409 when cancelled / no active PM / no
        active processor connection, and 502 when the adapter throws.
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_cents:
                  type: integer
                  minimum: 0
                  exclusiveMinimum: true
                reason:
                  type: string
                  minLength: 1
                currency:
                  type: string
              required:
                - amount_cents
                - reason
      responses:
        "200":
          description: Charge resolved (succeeded or declined — check `ok`)
          content:
            application/json:
              schema:
                anyOf:
                  - type: object
                    properties:
                      ok:
                        type: boolean
                        enum:
                          - true
                      manual_charge_id:
                        type: string
                        format: uuid
                      charge_id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        enum:
                          - succeeded
                      processor_transaction_id:
                        type: string
                      amount_cents:
                        type: integer
                      currency:
                        type: string
                    required:
                      - ok
                      - manual_charge_id
                      - charge_id
                      - status
                      - processor_transaction_id
                      - amount_cents
                      - currency
                  - type: object
                    properties:
                      ok:
                        type: boolean
                        enum:
                          - false
                      manual_charge_id:
                        type: string
                        format: uuid
                      charge_id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        enum:
                          - failed
                      failure_code:
                        type: string
                      failure_message:
                        type: string
                    required:
                      - ok
                      - manual_charge_id
                      - charge_id
                      - status
                      - failure_code
                      - failure_message
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: Subscriber not opted in to unscheduled charges
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Cancelled / no active payment method / no active processor connection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (amount_cents / reason)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: Processor charge failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/charges/{id}/retry-materialization:
    post:
      tags:
        - charges
        - admin
      summary: Re-attempt BC order creation for a succeeded charge with no order ID
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/dashboard/summary:
    get:
      tags:
        - dashboard
      summary: Dashboard summary (MRR, active subs, recent charges)
      security: *a1
      responses:
        "200":
          description: Summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  active_subscriptions:
                    type: integer
                  mrr_cents:
                    type: integer
                  recent_charges:
                    type: array
                    items:
                      $ref: "#/components/schemas/Charge"
                required:
                  - active_subscriptions
                  - mrr_cents
                  - recent_charges
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/dashboard:
    get:
      tags:
        - dashboard
        - admin
      summary: Admin dashboard — MRR, churn, exceptions, at-risk
      security: *a1
      responses:
        "200":
          description: Admin dashboard payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  mrr_cents:
                    type: integer
                  churn_rate:
                    type: number
                  failed_count:
                    type: integer
                  exceptions_count:
                    type: integer
                required:
                  - mrr_cents
                  - churn_rate
                  - failed_count
                  - exceptions_count
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/exceptions:
    get:
      tags:
        - exceptions
        - admin
      summary: List exceptions queue
      security: *a1
      parameters:
        - schema:
            type: string
            enum:
              - payment_failed
              - oos_renewal_blocked
          required: false
          name: type
          in: query
        - schema:
            type: string
            enum:
              - low
              - medium
              - high
          required: false
          name: severity
          in: query
        - schema:
            type: string
            enum:
              - impact
              - age
          required: false
          name: sort
          in: query
      responses:
        "200":
          description: Exceptions list
          content:
            application/json:
              schema:
                type: object
                properties:
                  exceptions:
                    type: array
                    items:
                      type: object
                      properties:
                        charge_id:
                          type: string
                          format: uuid
                        subscription_id:
                          type: string
                          format: uuid
                        status:
                          type: string
                          enum:
                            - open
                            - resolved
                        amount_cents:
                          type: integer
                        failed_at:
                          type: string
                          nullable: true
                          format: date-time
                      required:
                        - charge_id
                        - subscription_id
                        - status
                        - amount_cents
                        - failed_at
                required:
                  - exceptions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/exceptions/{id}/resolve:
    post:
      tags:
        - exceptions
        - admin
      summary: Resolve an exception queue item with a required note
      description: US-21.3 — marks the exception (keyed by the charge id that surfaced
        it) resolved, removes it from the queue, and emits an exception.resolved
        audit event. Migrated to the /api/v1/ prefix per US-27.1 (#1653).
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  minLength: 1
              required:
                - note
      responses:
        "200":
          description: Exception resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - open
                      - resolved
                required:
                  - id
                  - status
        "400":
          description: Invalid body — note is required and must be non-empty
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Exception already resolved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/reconciliation/latest:
    get:
      tags:
        - reconciliation
        - admin
      summary: Latest reconciliation run + drift entries
      security: *a1
      responses:
        "200":
          description: Reconciliation result
          content:
            application/json:
              schema:
                type: object
                properties:
                  run_at:
                    type: string
                    nullable: true
                    format: date-time
                  drift_entries:
                    type: array
                    items:
                      type: object
                      properties:
                        charge_id:
                          type: string
                          format: uuid
                        local_status:
                          type: string
                        bc_status:
                          type: string
                          nullable: true
                      required:
                        - charge_id
                        - local_status
                        - bc_status
                required:
                  - run_at
                  - drift_entries
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/product-exclusions:
    get:
      tags:
        - eligibility
        - admin
      summary: List product exclusions
      security: *a1
      responses:
        "200":
          description: Exclusions
          content:
            application/json:
              schema:
                type: object
                properties:
                  exclusions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        bc_product_id:
                          type: integer
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - bc_product_id
                        - created_at
                required:
                  - exclusions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - eligibility
        - admin
      summary: Add a product exclusion
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                bc_product_id:
                  type: integer
              required:
                - bc_product_id
      responses:
        "201":
          description: Added
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                required:
                  - id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already excluded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/product-exclusions/{id}:
    delete:
      tags:
        - eligibility
        - admin
      summary: Remove a product exclusion
      security: *a1
      parameters:
        - schema:
            type: string
            description: Numeric row ID
          required: true
          description: Numeric row ID
          name: id
          in: path
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/category-inclusions:
    get:
      tags:
        - eligibility
        - admin
      summary: List category inclusions
      security: *a1
      responses:
        "200":
          description: Inclusions
          content:
            application/json:
              schema:
                type: object
                properties:
                  inclusions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        bc_category_id:
                          type: integer
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - bc_category_id
                        - created_at
                required:
                  - inclusions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - eligibility
        - admin
      summary: Add a category inclusion
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                bc_category_id:
                  type: integer
              required:
                - bc_category_id
      responses:
        "201":
          description: Added
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                required:
                  - id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already included
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/category-inclusions/{id}:
    delete:
      tags:
        - eligibility
        - admin
      summary: Remove a category inclusion
      security: *a1
      parameters:
        - schema:
            type: string
            description: Numeric row ID
          required: true
          description: Numeric row ID
          name: id
          in: path
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/products:
    get:
      tags:
        - catalog
        - admin
      summary: List BC catalog products (proxy)
      security: *a1
      responses:
        "200":
          description: Products
          content:
            application/json:
              schema:
                type: object
                properties:
                  products:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: number
                        name:
                          type: string
                      required:
                        - id
                        - name
                      additionalProperties:
                        nullable: true
                required:
                  - products
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/products/{id}/variants:
    get:
      tags:
        - catalog
        - admin
      summary: List variants for a BC product (proxy)
      security: *a1
      parameters:
        - schema:
            type: string
            description: Numeric row ID
          required: true
          description: Numeric row ID
          name: id
          in: path
      responses:
        "200":
          description: Variants
          content:
            application/json:
              schema:
                type: object
                properties:
                  variants:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: number
                        sku:
                          type: string
                        price:
                          type: number
                      required:
                        - id
                        - sku
                        - price
                      additionalProperties:
                        nullable: true
                required:
                  - variants
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/onboarding/health:
    get:
      tags:
        - onboarding
      summary: App Extensions + webhook registration health
      security: *a1
      responses:
        "200":
          description: Health status
          content:
            application/json:
              schema:
                type: object
                properties:
                  extensions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        registered:
                          type: boolean
                      required:
                        - id
                        - registered
                  webhooks:
                    type: array
                    items:
                      type: object
                      properties:
                        scope:
                          type: string
                        registered:
                          type: boolean
                      required:
                        - scope
                        - registered
                required:
                  - extensions
                  - webhooks
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/onboarding/checklist:
    get:
      tags:
        - onboarding
      summary: Setup checklist items
      security: *a1
      responses:
        "200":
          description: Checklist
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        complete:
                          type: boolean
                        required:
                          type: boolean
                      required:
                        - key
                        - complete
                        - required
                  dismissed_at:
                    type: string
                    nullable: true
                    format: date-time
                required:
                  - items
                  - dismissed_at
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/checklist/dismiss:
    post:
      tags:
        - onboarding
      summary: Dismiss the onboarding checklist
      security: *a1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/registration-health/re-register:
    post:
      tags:
        - admin
        - health
      summary: Re-register a specific App Extension
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                extension_id:
                  type: string
              required:
                - extension_id
      responses:
        "200":
          description: Re-registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/registration-health/re-subscribe:
    post:
      tags:
        - admin
        - health
      summary: Re-subscribe a specific webhook
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                webhook_id:
                  type: string
              required:
                - webhook_id
      responses:
        "200":
          description: Re-subscribed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/registration-health/re-register-all:
    post:
      tags:
        - admin
        - health
      summary: Re-register all App Extensions and re-subscribe all webhooks
      security: *a1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/registration-health/re-mint-storefront-token:
    post:
      tags:
        - admin
        - health
      summary: Re-mint the per-merchant BC Storefront API token
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                store_hash:
                  type: string
      responses:
        "200":
          description: Re-minted; row updated with fresh ciphertext + expiry
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  expires_at:
                    type: integer
                  scope_present:
                    type: boolean
                    enum:
                      - true
                required:
                  - success
                  - expires_at
                  - scope_present
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "412":
          description: Precondition failed — scope not granted or access token unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limited (5/hr/key)
          headers:
            Retry-After:
              description: Seconds before the next attempt is permitted.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Internal failure (crypto / encrypt)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                      - internal
                required:
                  - error
        "502":
          description: Upstream BigCommerce mint failure
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/b2b-enrollment:
    post:
      tags:
        - admin
        - b2b
      summary: CS-rep enrolls a B2B buyer on behalf of a merchant
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_email:
                  type: string
                  format: email
                company_name:
                  type: string
              required:
                - customer_email
                - company_name
      responses:
        "200":
          description: Enrolled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/dsar/{customer_id}/export:
    get:
      tags:
        - compliance
        - admin
      summary: Export all subscription data for a customer (DSAR)
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: customer_id
          in: path
      responses:
        "200":
          description: Customer data export
          content:
            application/json:
              schema:
                type: object
                properties:
                  customer:
                    type: object
                    additionalProperties:
                      nullable: true
                  subscriptions:
                    type: array
                    items:
                      type: object
                      additionalProperties:
                        nullable: true
                required:
                  - customer
                  - subscriptions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/dsar/{customer_id}/erase:
    post:
      tags:
        - compliance
        - admin
      summary: Soft-delete (tombstone) all data for a customer (DSAR erasure)
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: customer_id
          in: path
      responses:
        "200":
          description: Erased
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/dunning/policy:
    get:
      tags:
        - charges
        - admin
      summary: List retry stages for the store dunning policy
      security: *a1
      responses:
        "200":
          description: Dunning policy stages
          content:
            application/json:
              schema:
                type: object
                properties:
                  stages:
                    type: array
                    items:
                      type: object
                      properties:
                        stage:
                          type: integer
                        delay_hours:
                          type: integer
                        action:
                          type: string
                      required:
                        - stage
                        - delay_hours
                        - action
                required:
                  - stages
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - charges
        - admin
      summary: Replace all dunning policy stages
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                stages:
                  type: array
                  items:
                    type: object
                    properties:
                      stage:
                        type: integer
                      delay_hours:
                        type: integer
                      action:
                        type: string
                    required:
                      - stage
                      - delay_hours
                      - action
              required:
                - stages
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/dunning/policy/reset:
    post:
      tags:
        - charges
        - admin
      summary: Reset dunning policy to system defaults
      security: *a1
      responses:
        "200":
          description: Reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/store/settings:
    get:
      tags:
        - admin
      summary: Get store-level settings (e.g. capture_timing)
      security: *a1
      responses:
        "200":
          description: Store settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  capture_timing:
                    type: string
                    enum:
                      - immediate
                      - on_ship
                required:
                  - capture_timing
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - admin
      summary: Update store-level settings
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                capture_timing:
                  type: string
                  enum:
                    - immediate
                    - on_ship
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/webhook-subscriptions:
    get:
      tags:
        - admin
      summary: List registered outbound webhook subscriptions
      security: *a1
      responses:
        "200":
          description: Webhook subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        url:
                          type: string
                          format: uri
                        event_types:
                          type: array
                          items:
                            type: string
                        active:
                          type: boolean
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - url
                        - event_types
                        - active
                        - created_at
                required:
                  - subscriptions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - admin
      summary: Register a new outbound webhook subscription
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                event_types:
                  type: array
                  items:
                    type: string
                  minItems: 1
              required:
                - url
                - event_types
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                required:
                  - id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/webhook-subscriptions/{id}:
    get:
      tags:
        - admin
      summary: Get a webhook subscription by ID
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Webhook subscription
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  url:
                    type: string
                    format: uri
                  event_types:
                    type: array
                    items:
                      type: string
                  active:
                    type: boolean
                  created_at:
                    type: string
                    format: date-time
                required:
                  - id
                  - url
                  - event_types
                  - active
                  - created_at
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    delete:
      tags:
        - admin
      summary: Delete (deregister) a webhook subscription
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/audit-log:
    get:
      tags:
        - audit
        - admin
      summary: Paginated audit event log for the store
      security: *a1
      parameters:
        - schema:
            type: integer
            minimum: 1
            default: 1
          required: false
          name: page
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          required: false
          name: page_size
          in: query
        - schema:
            type: string
          required: false
          name: type
          in: query
      responses:
        "200":
          description: Audit log entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        type:
                          type: string
                        actor_kind:
                          type: string
                          enum:
                            - merchant_user
                            - subscriber
                            - system
                            - webhook_bc
                            - webhook_processor
                        payload:
                          type: object
                          nullable: true
                          additionalProperties:
                            nullable: true
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - type
                        - actor_kind
                        - payload
                        - created_at
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      page:
                        type: integer
                      page_size:
                        type: integer
                    required:
                      - total
                      - page
                      - page_size
                required:
                  - events
                  - pagination
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/analytics/cohort:
    get:
      tags:
        - admin
      summary: Cohort retention analytics for subscriptions
      security: *a1
      responses:
        "200":
          description: Cohort data
          content:
            application/json:
              schema:
                type: object
                properties:
                  cohorts:
                    type: array
                    items:
                      type: object
                      additionalProperties:
                        nullable: true
                required:
                  - cohorts
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/analytics/ltv:
    get:
      tags:
        - admin
      summary: Lifetime value analytics per customer segment
      security: *a1
      responses:
        "200":
          description: LTV data
          content:
            application/json:
              schema:
                type: object
                properties:
                  segments:
                    type: array
                    items:
                      type: object
                      additionalProperties:
                        nullable: true
                required:
                  - segments
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /webhooks/stripe:
    post:
      tags:
        - webhooks
      summary: Stripe webhook receiver
      description: Public endpoint. Verifies the Stripe-Signature HMAC over
        `t.<rawbody>` before processing payment_intent.succeeded/failed,
        charge.refunded, charge.dispute.*. Returns 401 on invalid signature, 500
        on handler error.
      responses:
        "200":
          description: Received
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    enum:
                      - true
                required:
                  - received
        "401":
          description: Invalid signature
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Handler error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                required:
                  - error
  /api/v1/admin/bundles:
    get:
      tags:
        - admin
        - bundles
      summary: List order bundles
      description: "Epic-16 US-16.3 (Hive #885 / ADR-0049). Cursor-paginated."
      security: *a1
      parameters:
        - schema:
            type: string
          required: false
          name: cursor
          in: query
        - schema:
            type: string
          required: false
          name: page_size
          in: query
      responses:
        "200":
          description: Bundle list
          content:
            application/json:
              schema:
                type: object
                properties:
                  bundles:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        anchor_date:
                          type: string
                        bc_order_id:
                          type: integer
                          nullable: true
                        window_days:
                          type: integer
                        member_count:
                          type: integer
                        included_count:
                          type: integer
                        total_amount_cents:
                          type: integer
                        currency:
                          type: string
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - anchor_date
                        - bc_order_id
                        - window_days
                        - member_count
                        - included_count
                        - total_amount_cents
                        - currency
                        - created_at
                        - updated_at
                  next_cursor:
                    type: string
                    nullable: true
                  page_size:
                    type: integer
                required:
                  - bundles
                  - next_cursor
                  - page_size
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/bundles/{id}:
    get:
      tags:
        - admin
        - bundles
      summary: Get bundle detail with members
      description: Epic-16 US-16.3. {id} is the bundle_id. Returns the bundle summary
        plus member charges with subscription/customer/plan joins.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Bundle detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  bundle:
                    type: object
                    properties:
                      id:
                        type: string
                      anchor_date:
                        type: string
                      bc_order_id:
                        type: integer
                        nullable: true
                      window_days:
                        type: integer
                      member_count:
                        type: integer
                      included_count:
                        type: integer
                      total_amount_cents:
                        type: integer
                      currency:
                        type: string
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
                    required:
                      - id
                      - anchor_date
                      - bc_order_id
                      - window_days
                      - member_count
                      - included_count
                      - total_amount_cents
                      - currency
                      - created_at
                      - updated_at
                  members:
                    type: array
                    items:
                      type: object
                      additionalProperties:
                        nullable: true
                required:
                  - bundle
                  - members
                additionalProperties:
                  nullable: true
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/processor-connections/auto-detect:
    post:
      tags:
        - admin
        - processors
      summary: Auto-detect and connect a store payment processor
      description: Probes the store for a supported processor. Returns
        detected/alreadyActive flags and a connectionId when a new connection is
        created (201) vs already present (200).
      security: *a1
      responses:
        "200":
          description: Detection result (no new connection created)
          content:
            application/json:
              schema:
                type: object
                properties:
                  detected:
                    type: boolean
                  alreadyActive:
                    type: boolean
                  connectionId:
                    type: string
                    nullable: true
                required:
                  - detected
                  - alreadyActive
                  - connectionId
        "201":
          description: Detected and connected (new connection)
          content:
            application/json:
              schema:
                type: object
                properties:
                  detected:
                    type: boolean
                    enum:
                      - true
                  alreadyActive:
                    type: boolean
                    enum:
                      - false
                  connectionId:
                    type: string
                required:
                  - detected
                  - alreadyActive
                  - connectionId
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: Auto-detect failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/processor-connections:
    get:
      tags:
        - admin
        - processors
      summary: List processor connections
      description: Secret keys are never returned.
      security: *a1
      responses:
        "200":
          description: Connections
          content:
            application/json:
              schema:
                type: object
                properties:
                  connections:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        store_hash:
                          type: string
                        processor:
                          type: string
                        account_ref:
                          type: string
                          nullable: true
                        status:
                          type: string
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - store_hash
                        - processor
                        - account_ref
                        - status
                        - created_at
                        - updated_at
                required:
                  - connections
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - admin
        - processors
      summary: Create a Stripe processor connection
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                processor:
                  type: string
                  enum:
                    - stripe
                stripe_publishable_key:
                  type: string
                stripe_secret_key:
                  type: string
              required:
                - processor
                - stripe_publishable_key
                - stripe_secret_key
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  connection:
                    type: object
                    properties:
                      id:
                        type: string
                      store_hash:
                        type: string
                      processor:
                        type: string
                      account_ref:
                        type: string
                        nullable: true
                      status:
                        type: string
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
                    required:
                      - id
                      - store_hash
                      - processor
                      - account_ref
                      - status
                      - created_at
                      - updated_at
                required:
                  - connection
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/custom-fields:
    get:
      tags:
        - admin
        - custom-fields
      summary: List custom-field definitions
      description: US-6.11. Filterable by scope (plan|subscription|grant) and scope_id.
      security: *a1
      parameters:
        - schema:
            type: string
            enum:
              - plan
              - subscription
              - grant
          required: false
          name: scope
          in: query
        - schema:
            type: string
          required: false
          name: scope_id
          in: query
      responses:
        "200":
          description: Definitions
          content:
            application/json:
              schema:
                type: object
                properties:
                  definitions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        store_hash:
                          type: string
                        scope:
                          type: string
                          enum:
                            - plan
                            - subscription
                            - grant
                        scope_id:
                          type: string
                        field_key:
                          type: string
                        field_type:
                          type: string
                          enum:
                            - text
                            - number
                            - boolean
                            - select
                        field_label:
                          type: string
                        required:
                          type: integer
                        validation_regex:
                          type: string
                          nullable: true
                        options:
                          type: string
                          nullable: true
                        subscriber_visibility:
                          type: string
                          enum:
                            - hidden
                            - read
                            - write
                        display_order:
                          type: integer
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - store_hash
                        - scope
                        - scope_id
                        - field_key
                        - field_type
                        - field_label
                        - required
                        - validation_regex
                        - options
                        - subscriber_visibility
                        - display_order
                        - created_at
                      additionalProperties:
                        nullable: true
                required:
                  - definitions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - admin
        - custom-fields
      summary: Create a custom-field definition
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                scope:
                  type: string
                  enum:
                    - plan
                    - subscription
                    - grant
                scope_id:
                  type: string
                field_key:
                  type: string
                  description: ^[a-z][a-z0-9_]{0,63}$
                field_type:
                  type: string
                  enum:
                    - text
                    - number
                    - boolean
                    - select
                field_label:
                  type: string
                required:
                  type: boolean
                validation_regex:
                  type: string
                  nullable: true
                options:
                  type: array
                  nullable: true
                  items:
                    type: string
                subscriber_visibility:
                  type: string
                  enum:
                    - hidden
                    - read
                    - write
                display_order:
                  type: integer
              required:
                - scope
                - scope_id
                - field_key
                - field_type
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  definition:
                    type: object
                    properties:
                      id:
                        type: string
                      store_hash:
                        type: string
                      scope:
                        type: string
                        enum:
                          - plan
                          - subscription
                          - grant
                      scope_id:
                        type: string
                      field_key:
                        type: string
                      field_type:
                        type: string
                        enum:
                          - text
                          - number
                          - boolean
                          - select
                      field_label:
                        type: string
                      required:
                        type: integer
                      validation_regex:
                        type: string
                        nullable: true
                      options:
                        type: string
                        nullable: true
                      subscriber_visibility:
                        type: string
                        enum:
                          - hidden
                          - read
                          - write
                      display_order:
                        type: integer
                      created_at:
                        type: string
                        format: date-time
                    required:
                      - id
                      - store_hash
                      - scope
                      - scope_id
                      - field_key
                      - field_type
                      - field_label
                      - required
                      - validation_regex
                      - options
                      - subscriber_visibility
                      - display_order
                      - created_at
                    additionalProperties:
                      nullable: true
                required:
                  - definition
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Duplicate definition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/custom-fields/{id}:
    put:
      tags:
        - admin
        - custom-fields
      summary: Update a custom-field definition
      description: "Mutable: field_label, required, validation_regex, options,
        subscriber_visibility, display_order."
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: {}
              additionalProperties:
                nullable: true
      responses:
        "200":
          description: Updated (or no-op)
          content:
            application/json:
              schema:
                anyOf:
                  - type: object
                    properties:
                      definition:
                        type: object
                        properties:
                          id:
                            type: string
                          store_hash:
                            type: string
                          scope:
                            type: string
                            enum:
                              - plan
                              - subscription
                              - grant
                          scope_id:
                            type: string
                          field_key:
                            type: string
                          field_type:
                            type: string
                            enum:
                              - text
                              - number
                              - boolean
                              - select
                          field_label:
                            type: string
                          required:
                            type: integer
                          validation_regex:
                            type: string
                            nullable: true
                          options:
                            type: string
                            nullable: true
                          subscriber_visibility:
                            type: string
                            enum:
                              - hidden
                              - read
                              - write
                          display_order:
                            type: integer
                          created_at:
                            type: string
                            format: date-time
                        required:
                          - id
                          - store_hash
                          - scope
                          - scope_id
                          - field_key
                          - field_type
                          - field_label
                          - required
                          - validation_regex
                          - options
                          - subscriber_visibility
                          - display_order
                          - created_at
                        additionalProperties:
                          nullable: true
                    required:
                      - definition
                  - type: object
                    properties:
                      ok:
                        type: boolean
                        enum:
                          - true
                      message:
                        type: string
                    required:
                      - ok
                      - message
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    delete:
      tags:
        - admin
        - custom-fields
      summary: Delete a custom-field definition
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/settings/store:
    get:
      tags:
        - admin
        - settings
      summary: Get the full store-settings view
      description: "Hive #1070. Umbrella settings (capture timing, feature-flag
        fields, order defaults)."
      security: *a1
      responses:
        "200":
          description: Store settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  capture_timing:
                    type: string
                  country_code:
                    type: string
                    nullable: true
                  test_mode_enabled:
                    type: integer
                  nudge_lead_days:
                    type: integer
                  sync_order_edits_to_subscription:
                    type: integer
                  bundle_window_days:
                    type: integer
                    nullable: true
                  allow_mixed_bundle:
                    type: integer
                  reactivation_grace_days:
                    type: integer
                    nullable: true
                  default_sub_status_id:
                    type: integer
                    nullable: true
                required:
                  - capture_timing
                  - country_code
                  - test_mode_enabled
                  - nudge_lead_days
                  - sync_order_edits_to_subscription
                  - bundle_window_days
                  - allow_mixed_bundle
                  - reactivation_grace_days
                  - default_sub_status_id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    patch:
      tags:
        - admin
        - settings
      summary: Partially update store settings
      description: "Hive #1070. Only present keys are updated; the response echoes
        changed_keys and emits settings.changed when a value flips."
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                capture_timing:
                  type: string
                test_mode_enabled:
                  type: integer
                nudge_lead_days:
                  type: integer
                sync_order_edits_to_subscription:
                  type: integer
                bundle_window_days:
                  type: integer
                  nullable: true
                allow_mixed_bundle:
                  type: integer
                reactivation_grace_days:
                  type: integer
                  nullable: true
                default_sub_status_id:
                  type: integer
                  nullable: true
      responses:
        "200":
          description: Updated settings + changed_keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  capture_timing:
                    type: string
                  country_code:
                    type: string
                    nullable: true
                  test_mode_enabled:
                    type: integer
                  nudge_lead_days:
                    type: integer
                  sync_order_edits_to_subscription:
                    type: integer
                  bundle_window_days:
                    type: integer
                    nullable: true
                  allow_mixed_bundle:
                    type: integer
                  reactivation_grace_days:
                    type: integer
                    nullable: true
                  default_sub_status_id:
                    type: integer
                    nullable: true
                  changed_keys:
                    type: array
                    items:
                      type: string
                required:
                  - capture_timing
                  - country_code
                  - test_mode_enabled
                  - nudge_lead_days
                  - sync_order_edits_to_subscription
                  - bundle_window_days
                  - allow_mixed_bundle
                  - reactivation_grace_days
                  - default_sub_status_id
                  - changed_keys
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/store/feature-flags:
    get:
      tags:
        - admin
        - settings
      summary: Get the store feature-flag bundle
      description: "Hive #1062. Currently admin_shell_v2."
      security: *a1
      responses:
        "200":
          description: Flag bundle
          content:
            application/json:
              schema:
                type: object
                properties:
                  admin_shell_v2:
                    type: integer
                    description: 0 | 1
                required:
                  - admin_shell_v2
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - admin
        - settings
      summary: Toggle the admin_shell_v2 feature flag
      description: "Hive #1062. Emits settings.changed on EVENTS_QUEUE when the value
        flips."
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                admin_shell_v2:
                  type: integer
                  description: 0 | 1
              required:
                - admin_shell_v2
      responses:
        "200":
          description: Updated flags
          content:
            application/json:
              schema:
                type: object
                properties:
                  admin_shell_v2:
                    type: integer
                required:
                  - admin_shell_v2
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/store-capabilities:
    get:
      tags:
        - admin
        - settings
      summary: Get external-dependency capability signals
      description: "Hive #1125. Aggregates capability probes (today: B2B Edition) so
        the SPA can gate optional surfaces. Defaults to installed/enabled when a
        probe is indeterminate (back-compat)."
      security: *a1
      responses:
        "200":
          description: Capabilities
          content:
            application/json:
              schema:
                type: object
                properties:
                  b2b_edition:
                    type: object
                    properties:
                      installed:
                        type: boolean
                      enabled:
                        type: boolean
                    required:
                      - installed
                      - enabled
                required:
                  - b2b_edition
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/channels:
    get:
      tags:
        - admin
        - catalog
      summary: List storefront channels (BC proxy)
      description: Proxies BC GET /v3/channels via the stored OAuth token and passes
        through BC’s `{ data, meta }` envelope.
      security: *a1
      responses:
        "200":
          description: BC channels envelope
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      additionalProperties:
                        nullable: true
                  meta:
                    type: object
                    additionalProperties:
                      nullable: true
                required:
                  - data
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: No store access token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: BC upstream error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/products/{id}:
    get:
      tags:
        - admin
        - catalog
      summary: Get a single product (BC proxy)
      description: "Hive #569. Proxies BC’s single-product endpoint. {id} is the
        numeric BC product id. Degrades to source=fallback_empty when no OAuth
        token is on file."
      security: *a1
      parameters:
        - schema:
            type: string
            description: numeric BC product id
          required: true
          description: numeric BC product id
          name: id
          in: path
      responses:
        "200":
          description: Product (or fallback_empty)
          content:
            application/json:
              schema:
                anyOf:
                  - type: object
                    properties:
                      product:
                        type: object
                        properties:
                          bc_product_id:
                            type: integer
                          name:
                            type: string
                          brand_id:
                            type: integer
                            nullable: true
                          base_price_cents:
                            type: integer
                          sku:
                            type: string
                        required:
                          - bc_product_id
                          - name
                          - brand_id
                          - base_price_cents
                          - sku
                        additionalProperties:
                          nullable: true
                      source:
                        type: string
                        enum:
                          - bc_api
                    required:
                      - product
                      - source
                  - type: object
                    properties:
                      products:
                        type: array
                        items:
                          nullable: true
                      pagination:
                        type: object
                        additionalProperties:
                          nullable: true
                      source:
                        type: string
                        enum:
                          - fallback_empty
                    required:
                      - products
                      - pagination
                      - source
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/currencies:
    get:
      tags:
        - admin
        - catalog
      summary: List store currencies (BC proxy)
      description: "Hive #1072. Proxies BC /v2/currencies. Degrades to a USD default
        with source=fallback_empty when no OAuth token is on file."
      security: *a1
      responses:
        "200":
          description: Currencies
          content:
            application/json:
              schema:
                type: object
                properties:
                  currencies:
                    type: array
                    items:
                      type: object
                      properties:
                        currency_code:
                          type: string
                        name:
                          type: string
                        is_default:
                          type: boolean
                        token:
                          type: string
                      required:
                        - currency_code
                        - name
                        - is_default
                        - token
                  source:
                    type: string
                    enum:
                      - bc_api
                      - fallback_empty
                required:
                  - currencies
                  - source
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/plans/{id}/copy:
    post:
      tags:
        - admin
        - plans
      summary: Copy a plan onto multiple target products
      description: Epic-4 US-4.5. Per-target outcome; a target that already has a plan
        fails with reason=already_has_plan.
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                target_bc_product_ids:
                  type: array
                  items:
                    type: integer
                    minimum: 0
                    exclusiveMinimum: true
                  minItems: 1
              required:
                - target_bc_product_ids
      responses:
        "200":
          description: Per-target copy outcome
          content:
            application/json:
              schema:
                type: object
                properties:
                  succeeded:
                    type: array
                    items:
                      type: object
                      properties:
                        bc_product_id:
                          type: integer
                        new_plan_id:
                          type: string
                          format: uuid
                      required:
                        - bc_product_id
                        - new_plan_id
                  failed:
                    type: array
                    items:
                      type: object
                      properties:
                        bc_product_id:
                          type: integer
                        reason:
                          type: string
                      required:
                        - bc_product_id
                        - reason
                required:
                  - succeeded
                  - failed
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/plans/{id}/coverage:
    get:
      tags:
        - admin
        - plans
      summary: Get a plan’s scoping coverage
      description: Returns the channel / customer-group / country / price-list scoping
        arrays for the plan.
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Coverage
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan_id:
                    type: string
                    format: uuid
                  channels:
                    type: array
                    items:
                      type: string
                  customer_groups:
                    type: array
                    items:
                      type: string
                  countries:
                    type: array
                    items:
                      type: string
                  price_lists:
                    type: array
                    items:
                      type: string
                required:
                  - plan_id
                  - channels
                  - customer_groups
                  - countries
                  - price_lists
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/plans/{id}/sales-mode-overrides:
    get:
      tags:
        - admin
        - plans
      summary: List per-channel / per-customer-group sales-mode overrides
      description: "Hive #1421/#1420. Both override axes for the plan."
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Overrides
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan_id:
                    type: string
                    format: uuid
                  channel:
                    type: array
                    items:
                      type: object
                      properties:
                        bc_channel_id:
                          type: integer
                        sales_mode:
                          type: string
                          enum:
                            - subscribe_only
                            - subscribe_and_one_time
                            - one_time_only
                      required:
                        - bc_channel_id
                        - sales_mode
                  customer_group:
                    type: array
                    items:
                      type: object
                      properties:
                        bc_customer_group_id:
                          type: integer
                        sales_mode:
                          type: string
                          enum:
                            - subscribe_only
                            - subscribe_and_one_time
                            - one_time_only
                      required:
                        - bc_customer_group_id
                        - sales_mode
                required:
                  - plan_id
                  - channel
                  - customer_group
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - admin
        - plans
      summary: Upsert one sales-mode override
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                scope:
                  type: string
                  enum:
                    - channel
                    - customer_group
                scope_id:
                  type: integer
                  minimum: 0
                sales_mode:
                  type: string
                  enum:
                    - subscribe_only
                    - subscribe_and_one_time
                    - one_time_only
              required:
                - scope
                - scope_id
                - sales_mode
      responses:
        "200":
          description: Upserted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  plan_id:
                    type: string
                    format: uuid
                  scope:
                    type: string
                    enum:
                      - channel
                      - customer_group
                  scope_id:
                    type: integer
                  sales_mode:
                    type: string
                    enum:
                      - subscribe_only
                      - subscribe_and_one_time
                      - one_time_only
                required:
                  - ok
                  - plan_id
                  - scope
                  - scope_id
                  - sales_mode
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    delete:
      tags:
        - admin
        - plans
      summary: Delete one sales-mode override
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                scope:
                  type: string
                  enum:
                    - channel
                    - customer_group
                scope_id:
                  type: integer
                  minimum: 0
              required:
                - scope
                - scope_id
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  plan_id:
                    type: string
                    format: uuid
                  scope:
                    type: string
                    enum:
                      - channel
                      - customer_group
                  scope_id:
                    type: integer
                required:
                  - ok
                  - plan_id
                  - scope
                  - scope_id
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/charges/{id}/refund:
    post:
      tags:
        - admin
        - charges
      summary: Refund a charge
      description: "US-12.1. Refunds via the processor adapter. amount_cents is
        optional (absent = full refund). Idempotent: a charge already refunded
        returns 409."
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_cents:
                  type: integer
                  minimum: 0
                  exclusiveMinimum: true
                reason:
                  type: string
      responses:
        "200":
          description: Refunded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  charge_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - refunded
                  refund_transaction_id:
                    type: string
                  refunded_amount_cents:
                    type: integer
                required:
                  - ok
                  - charge_id
                  - status
                  - refund_transaction_id
                  - refunded_amount_cents
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already refunded / not refundable / missing processor transaction
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (amount_cents)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: Processor refund failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/store-credits:
    post:
      tags:
        - admin
        - credits
      summary: Issue store credit to a customer
      description: US-12.2. Issuance side; credit is applied at renewal by the scheduler.
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_id:
                  type: string
                  format: uuid
                amount_cents:
                  type: integer
                  minimum: 0
                  exclusiveMinimum: true
                currency:
                  type: string
                subscription_id:
                  type: string
                  nullable: true
                  format: uuid
                reason:
                  type: string
                expires_at:
                  type: string
                  format: date-time
                  description: future ISO timestamp
                source:
                  type: string
              required:
                - customer_id
                - amount_cents
      responses:
        "201":
          description: Credit issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  customer_id:
                    type: string
                    format: uuid
                  amount_cents:
                    type: integer
                  currency:
                    type: string
                  subscription_id:
                    type: string
                    nullable: true
                    format: uuid
                  status:
                    type: string
                    enum:
                      - active
                required:
                  - id
                  - customer_id
                  - amount_cents
                  - currency
                  - subscription_id
                  - status
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: expires_at must be a future timestamp
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/customers/{id}/store-credit-balance:
    get:
      tags:
        - admin
        - credits
      summary: Get a customer’s store-credit balance
      description: US-12.2. {id} is the customer_id.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Balance
          content:
            application/json:
              schema:
                type: object
                properties:
                  customer_id:
                    type: string
                    format: uuid
                  balance_cents:
                    type: integer
                required:
                  - customer_id
                  - balance_cents
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/api-keys:
    get:
      tags:
        - admin
        - api-keys
      summary: List API keys
      security: *a1
      responses:
        "200":
          description: API keys (no secrets)
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_keys:
                    type: array
                    items:
                      type: object
                      additionalProperties:
                        nullable: true
                required:
                  - api_keys
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - admin
        - api-keys
      summary: Create an API key
      description: The plaintext key is returned ONCE in the 201 response and is not
        stored — it cannot be recovered.
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                scopes:
                  type: array
                  items:
                    type: string
                  minItems: 1
              required:
                - name
                - scopes
      responses:
        "201":
          description: Created (plaintext key returned once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  key_prefix:
                    type: string
                  scopes:
                    type: array
                    items:
                      type: string
                  key:
                    type: string
                required:
                  - id
                  - key_prefix
                  - scopes
                  - key
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (name / scopes)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/api-keys/{id}:
    delete:
      tags:
        - admin
        - api-keys
      summary: Revoke an API key
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - revoked
                  id:
                    type: string
                required:
                  - status
                  - id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/onboarding/storefront/stencil/enable:
    post:
      tags:
        - admin
        - onboarding
      summary: Enable the Stencil storefront widget (Scripts API)
      description: "#1069. Registers the subscription widget via BC Scripts API and
        stores the script UUID. Idempotent — already-enabled returns
        already_enabled=true."
      security: *a1
      responses:
        "200":
          description: Enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  script_uuid:
                    type: string
                  enabled:
                    type: boolean
                    enum:
                      - true
                  already_enabled:
                    type: boolean
                required:
                  - script_uuid
                  - enabled
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: No store access token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: BC Scripts API error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/onboarding/storefront/stencil/disable:
    delete:
      tags:
        - admin
        - onboarding
      summary: Disable the Stencil storefront widget
      description: "#1069. Removes the Scripts API entry and clears the stored UUID.
        Idempotent — no UUID stored returns already_disabled=true."
      security: *a1
      responses:
        "200":
          description: Disabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                    enum:
                      - false
                  already_disabled:
                    type: boolean
                required:
                  - enabled
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: No store access token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/onboarding/extensions/re-register:
    post:
      tags:
        - admin
        - onboarding
      summary: Re-register App Extensions (ORDERS/PRODUCTS/CUSTOMERS panels)
      description: US-1.3. Idempotent retry of BC App Extension registration. Returns
        207 when some (not all) registrations fail.
      security: *a1
      responses:
        "200":
          description: All extensions registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - ok
                      - partial
                      - failed
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        model:
                          type: string
                        status:
                          type: string
                          enum:
                            - ok
                            - failed
                        message:
                          type: string
                          nullable: true
                      required:
                        - model
                        - status
                        - message
                required:
                  - status
                  - results
        "207":
          description: Partial / all failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - ok
                      - partial
                      - failed
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        model:
                          type: string
                        status:
                          type: string
                          enum:
                            - ok
                            - failed
                        message:
                          type: string
                          nullable: true
                      required:
                        - model
                        - status
                        - message
                required:
                  - status
                  - results
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: No store access token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/b2b-change-requests:
    get:
      tags:
        - admin
        - b2b
      summary: List B2B subscription change requests
      description: Epic-24 US-24.3. Lists buyer-submitted change requests for the
        store, filtered by status (default pending) and optionally
        subscription_id. Capped at 100 rows.
      security: *a1
      parameters:
        - schema:
            type: string
            enum:
              - pending
              - approved
              - rejected
              - applied
              - expired
            description: default pending
          required: false
          description: default pending
          name: status
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: subscription_id
          in: query
      responses:
        "200":
          description: Change request list
          content:
            application/json:
              schema:
                type: object
                properties:
                  change_requests:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        store_hash:
                          type: string
                        subscription_id:
                          type: string
                          format: uuid
                        customer_id:
                          type: string
                          format: uuid
                        change_type:
                          type: string
                          enum:
                            - pause
                            - cancel
                            - change_interval
                            - change_quantity
                            - update_payment
                        change_payload:
                          type: string
                          nullable: true
                          description: JSON-encoded change details
                        status:
                          type: string
                          enum:
                            - pending
                            - approved
                            - rejected
                            - applied
                            - expired
                        reviewer_user_id:
                          type: integer
                          nullable: true
                        reviewer_notes:
                          type: string
                          nullable: true
                        expires_at:
                          type: string
                          nullable: true
                          format: date-time
                        applied_at:
                          type: string
                          nullable: true
                          format: date-time
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - store_hash
                        - subscription_id
                        - customer_id
                        - change_type
                        - change_payload
                        - status
                        - reviewer_user_id
                        - reviewer_notes
                        - expires_at
                        - applied_at
                        - created_at
                        - updated_at
                required:
                  - change_requests
        "400":
          description: Invalid status filter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/b2b-change-requests/{id}/approve:
    post:
      tags:
        - admin
        - b2b
      summary: Approve a B2B change request
      description: Epic-24 US-24.3. Flips a pending request to approved (the gate; the
        actual mutation is applied separately via the portal mutation APIs). No
        request body. Returns 412 if the request is not pending.
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Approved
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  change_request_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - approved
                required:
                  - ok
                  - change_request_id
                  - status
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "412":
          description: Change request not pending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/b2b-change-requests/{id}/reject:
    post:
      tags:
        - admin
        - b2b
      summary: Reject a B2B change request
      description: Epic-24 US-24.3. Flips a pending request to rejected with optional
        reviewer notes. Returns 412 if the request is not pending.
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                notes:
                  type: string
      responses:
        "200":
          description: Rejected
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  change_request_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - rejected
                  notes:
                    type: string
                    nullable: true
                required:
                  - ok
                  - change_request_id
                  - status
                  - notes
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "412":
          description: Change request not pending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/b2b-po-enrollment:
    post:
      tags:
        - admin
        - b2b
      summary: Enrol a B2B customer on net-terms / purchase-order billing
      description: Epic-24 US-24.4. Creates a subscription with no payment method on
        file — the approved PO is the payment instrument. Renewals generate a BC
        order with payment_status=pending instead of charging a processor. The
        customer and plan must already exist.
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_email:
                  type: string
                  format: email
                plan_id:
                  type: string
                  format: uuid
                start_date:
                  type: string
                  description: YYYY-MM-DD
                b2b_company_name:
                  type: string
                po_number:
                  type: string
                  description: approved purchase order / contract reference
                net_terms_days:
                  type: integer
                  minimum: 1
                  maximum: 365
                  description: default 30
                notes:
                  type: string
              required:
                - customer_email
                - plan_id
                - start_date
                - b2b_company_name
                - po_number
      responses:
        "201":
          description: Enrolled
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription_id:
                    type: string
                    format: uuid
                  customer_id:
                    type: string
                    format: uuid
                  company_name:
                    type: string
                  po_number:
                    type: string
                  net_terms_days:
                    type: integer
                required:
                  - subscription_id
                  - customer_id
                  - company_name
                  - po_number
                  - net_terms_days
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Customer or plan not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "412":
          description: Plan not active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/promotions:
    get:
      tags:
        - admin
        - promotions
      summary: List BC promotions annotated with the subscription_only flag
      description: Epic-25 US-25.1. Proxies BC GET /v3/promotions and joins our
        promotion_settings to annotate each with subscription_only. Degrades to
        an empty list (200, source=fallback_empty) when the store access token
        is unresolvable or BC returns 401/403 (missing store_v2_marketing scope)
        — the admin page renders an empty state, not an error.
      security: *a1
      parameters:
        - schema:
            type: string
            description: BC pagination page (default 1)
          required: false
          description: BC pagination page (default 1)
          name: page
          in: query
        - schema:
            type: string
            description: BC pagination limit (default 50)
          required: false
          description: BC pagination limit (default 50)
          name: limit
          in: query
      responses:
        "200":
          description: Annotated promotions list
          content:
            application/json:
              schema:
                type: object
                properties:
                  promotions:
                    type: array
                    items:
                      type: object
                      properties:
                        bc_promotion_id:
                          type: integer
                        name:
                          type: string
                        redemption_type:
                          type: string
                          enum:
                            - AUTOMATIC
                            - COUPON
                        status:
                          type: string
                          enum:
                            - ENABLED
                            - DISABLED
                        start_date:
                          type: string
                          nullable: true
                        end_date:
                          type: string
                          nullable: true
                        max_uses:
                          type: integer
                          nullable: true
                        current_uses:
                          type: integer
                        subscription_only:
                          type: boolean
                      required:
                        - bc_promotion_id
                        - name
                        - redemption_type
                        - status
                        - start_date
                        - end_date
                        - max_uses
                        - current_uses
                        - subscription_only
                  meta:
                    type: object
                    nullable: true
                    additionalProperties:
                      nullable: true
                  source:
                    type: string
                    enum:
                      - fallback_empty
                  bc_status:
                    type: integer
                required:
                  - promotions
                  - meta
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: BC upstream error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  message:
                    type: string
                  bc_status:
                    type: integer
                required:
                  - error
                  - message
  /api/v1/admin/promotions/{id}/settings:
    patch:
      tags:
        - admin
        - promotions
      summary: Set the subscription_only flag on a BC promotion
      description: Epic-25 US-25.1. Upserts a promotion_settings row. {id} is the
        numeric BC promotion id (promotion_settings overlays BC promotions,
        which carry no custom metadata).
      security: *a1
      parameters:
        - schema:
            type: string
            description: numeric BC promotion id
          required: true
          description: numeric BC promotion id
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subscription_only:
                  type: boolean
              required:
                - subscription_only
      responses:
        "200":
          description: Updated flag
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  promotion_setting:
                    type: object
                    properties:
                      bc_promotion_id:
                        type: integer
                      store_hash:
                        type: string
                      subscription_only:
                        type: boolean
                      updated_at:
                        type: string
                        nullable: true
                        format: date-time
                    required:
                      - bc_promotion_id
                      - store_hash
                      - subscription_only
                      - updated_at
                required:
                  - ok
                  - promotion_setting
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (bc_promotion_id / subscription_only)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscription-promotions:
    get:
      tags:
        - admin
        - promotions
      summary: List cycle-aware subscription promotions
      description: "Epic-25 (Hive #891). Standalone cycle-scoped promo entity
        (separate from the BC flag overlay)."
      security: *a1
      parameters:
        - schema:
            type: string
            description: '"true" to include archived promos'
          required: false
          description: '"true" to include archived promos'
          name: include_archived
          in: query
      responses:
        "200":
          description: Subscription promotion list
          content:
            application/json:
              schema:
                type: object
                properties:
                  store_hash:
                    type: string
                  promotions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        store_hash:
                          type: string
                        name:
                          type: string
                        code:
                          type: string
                          nullable: true
                        application_window:
                          type: string
                          enum:
                            - first_cycle_only
                            - first_n_cycles
                            - all_cycles
                            - tiered_cycles
                            - event_triggered
                        config:
                          type: object
                          additionalProperties:
                            nullable: true
                        eligibility_rules:
                          type: object
                          additionalProperties:
                            nullable: true
                        discount_type:
                          type: string
                          enum:
                            - percent
                            - fixed_cents
                            - free_shipping
                            - gift_item
                        stacking_rule:
                          type: string
                          enum:
                            - exclusive
                            - stackable
                        lock_policy:
                          type: string
                        applies_to:
                          type: string
                          enum:
                            - subscription_only
                            - one_time_only
                            - both
                        recurring:
                          type: boolean
                        status:
                          type: string
                          enum:
                            - active
                            - paused
                            - archived
                        max_redemptions:
                          type: integer
                          nullable: true
                        current_redemptions:
                          type: integer
                        starts_at:
                          type: string
                          nullable: true
                          format: date-time
                        ends_at:
                          type: string
                          nullable: true
                          format: date-time
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                        archived_at:
                          type: string
                          nullable: true
                          format: date-time
                      required:
                        - id
                        - store_hash
                        - name
                        - code
                        - application_window
                        - config
                        - eligibility_rules
                        - discount_type
                        - stacking_rule
                        - lock_policy
                        - applies_to
                        - recurring
                        - status
                        - max_redemptions
                        - current_redemptions
                        - starts_at
                        - ends_at
                        - created_at
                        - updated_at
                        - archived_at
                required:
                  - store_hash
                  - promotions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - admin
        - promotions
      summary: Create a cycle-aware subscription promotion
      description: "Epic-25 (Hive #891). discount_type-specific config validation
        applies (free_shipping / gift_item — Hive #901). Returns 409 if the
        coupon code is already in use."
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                code:
                  type: string
                  nullable: true
                application_window:
                  type: string
                  enum:
                    - first_cycle_only
                    - first_n_cycles
                    - all_cycles
                    - tiered_cycles
                    - event_triggered
                config:
                  anyOf:
                    - type: object
                      additionalProperties:
                        nullable: true
                    - type: string
                eligibility_rules:
                  anyOf:
                    - type: object
                      additionalProperties:
                        nullable: true
                    - type: string
                discount_type:
                  type: string
                  enum:
                    - percent
                    - fixed_cents
                    - free_shipping
                    - gift_item
                stacking_rule:
                  type: string
                  enum:
                    - exclusive
                    - stackable
                applies_to:
                  type: string
                  enum:
                    - subscription_only
                    - one_time_only
                    - both
                recurring:
                  type: boolean
                status:
                  type: string
                  enum:
                    - active
                    - paused
                    - archived
                max_redemptions:
                  type: integer
                  nullable: true
                  minimum: 1
                starts_at:
                  type: string
                  nullable: true
                  format: date-time
                ends_at:
                  type: string
                  nullable: true
                  format: date-time
              required:
                - name
                - application_window
                - config
                - discount_type
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  store_hash:
                    type: string
                  promotion:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      store_hash:
                        type: string
                      name:
                        type: string
                      code:
                        type: string
                        nullable: true
                      application_window:
                        type: string
                        enum:
                          - first_cycle_only
                          - first_n_cycles
                          - all_cycles
                          - tiered_cycles
                          - event_triggered
                      config:
                        type: object
                        additionalProperties:
                          nullable: true
                      eligibility_rules:
                        type: object
                        additionalProperties:
                          nullable: true
                      discount_type:
                        type: string
                        enum:
                          - percent
                          - fixed_cents
                          - free_shipping
                          - gift_item
                      stacking_rule:
                        type: string
                        enum:
                          - exclusive
                          - stackable
                      lock_policy:
                        type: string
                      applies_to:
                        type: string
                        enum:
                          - subscription_only
                          - one_time_only
                          - both
                      recurring:
                        type: boolean
                      status:
                        type: string
                        enum:
                          - active
                          - paused
                          - archived
                      max_redemptions:
                        type: integer
                        nullable: true
                      current_redemptions:
                        type: integer
                      starts_at:
                        type: string
                        nullable: true
                        format: date-time
                      ends_at:
                        type: string
                        nullable: true
                        format: date-time
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
                      archived_at:
                        type: string
                        nullable: true
                        format: date-time
                    required:
                      - id
                      - store_hash
                      - name
                      - code
                      - application_window
                      - config
                      - eligibility_rules
                      - discount_type
                      - stacking_rule
                      - lock_policy
                      - applies_to
                      - recurring
                      - status
                      - max_redemptions
                      - current_redemptions
                      - starts_at
                      - ends_at
                      - created_at
                      - updated_at
                      - archived_at
                required:
                  - store_hash
                  - promotion
        "400":
          description: Invalid body (not JSON / not an object)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Coupon code already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscription-promotions/{id}/report:
    get:
      tags:
        - admin
        - promotions
      summary: Promotion performance + retention-lift report
      description: "Epic-25 US-25.9 (Hive #903). Aggregates redemptions, discount,
        attributed MRR, and a retention-lift comparison over a [from, to]
        window. retention_data.insufficient_data is true when total
        cancellations < 30. Requires `from` and `to` ISO-8601 query params."
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
        - schema:
            type: string
            description: ISO-8601 period start (required)
          required: true
          description: ISO-8601 period start (required)
          name: from
          in: query
        - schema:
            type: string
            description: ISO-8601 period end (required)
          required: true
          description: ISO-8601 period end (required)
          name: to
          in: query
      responses:
        "200":
          description: Promotion report
          content:
            application/json:
              schema:
                type: object
                properties:
                  promotion_id:
                    type: string
                    format: uuid
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                      to:
                        type: string
                    required:
                      - from
                      - to
                  redemptions:
                    type: integer
                  charges_discounted:
                    type: integer
                  total_discount_cents:
                    type: integer
                  active_subscriptions_with_promo:
                    type: integer
                  attributed_mrr_cents:
                    type: integer
                  source_breakdown:
                    type: object
                    properties:
                      subs:
                        type: object
                        properties:
                          charges_discounted:
                            type: integer
                          total_discount_cents:
                            type: integer
                        required:
                          - charges_discounted
                          - total_discount_cents
                      bc:
                        type: object
                        properties:
                          charges_discounted:
                            type: integer
                          total_discount_cents:
                            type: integer
                        required:
                          - charges_discounted
                          - total_discount_cents
                    required:
                      - subs
                      - bc
                  retention_data:
                    type: object
                    properties:
                      avg_cycles_to_cancel_with_promo:
                        type: number
                        nullable: true
                      avg_cycles_to_cancel_without_promo:
                        type: number
                        nullable: true
                      lift_ratio:
                        type: number
                        nullable: true
                      sample_size_with_promo:
                        type: integer
                      sample_size_without_promo:
                        type: integer
                      insufficient_data:
                        type: boolean
                    required:
                      - avg_cycles_to_cancel_with_promo
                      - avg_cycles_to_cancel_without_promo
                      - lift_ratio
                      - sample_size_with_promo
                      - sample_size_without_promo
                      - insufficient_data
                required:
                  - promotion_id
                  - period
                  - redemptions
                  - charges_discounted
                  - total_discount_cents
                  - active_subscriptions_with_promo
                  - attributed_mrr_cents
                  - source_breakdown
                  - retention_data
        "400":
          description: Missing/invalid from|to or from > to
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/subscription-promotions/{id}:
    get:
      tags:
        - admin
        - promotions
      summary: Get a subscription promotion by ID
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Subscription promotion
          content:
            application/json:
              schema:
                type: object
                properties:
                  store_hash:
                    type: string
                  promotion:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      store_hash:
                        type: string
                      name:
                        type: string
                      code:
                        type: string
                        nullable: true
                      application_window:
                        type: string
                        enum:
                          - first_cycle_only
                          - first_n_cycles
                          - all_cycles
                          - tiered_cycles
                          - event_triggered
                      config:
                        type: object
                        additionalProperties:
                          nullable: true
                      eligibility_rules:
                        type: object
                        additionalProperties:
                          nullable: true
                      discount_type:
                        type: string
                        enum:
                          - percent
                          - fixed_cents
                          - free_shipping
                          - gift_item
                      stacking_rule:
                        type: string
                        enum:
                          - exclusive
                          - stackable
                      lock_policy:
                        type: string
                      applies_to:
                        type: string
                        enum:
                          - subscription_only
                          - one_time_only
                          - both
                      recurring:
                        type: boolean
                      status:
                        type: string
                        enum:
                          - active
                          - paused
                          - archived
                      max_redemptions:
                        type: integer
                        nullable: true
                      current_redemptions:
                        type: integer
                      starts_at:
                        type: string
                        nullable: true
                        format: date-time
                      ends_at:
                        type: string
                        nullable: true
                        format: date-time
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
                      archived_at:
                        type: string
                        nullable: true
                        format: date-time
                    required:
                      - id
                      - store_hash
                      - name
                      - code
                      - application_window
                      - config
                      - eligibility_rules
                      - discount_type
                      - stacking_rule
                      - lock_policy
                      - applies_to
                      - recurring
                      - status
                      - max_redemptions
                      - current_redemptions
                      - starts_at
                      - ends_at
                      - created_at
                      - updated_at
                      - archived_at
                required:
                  - store_hash
                  - promotion
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - admin
        - promotions
      summary: Update a subscription promotion
      description: "Epic-25 (Hive #891). Mutable fields only — application_window,
        code, discount_type, and lock_policy are immutable (changing them
        returns 422; archive + recreate instead)."
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                status:
                  type: string
                  enum:
                    - active
                    - paused
                    - archived
                config:
                  anyOf:
                    - type: object
                      additionalProperties:
                        nullable: true
                    - type: string
                eligibility_rules:
                  anyOf:
                    - type: object
                      additionalProperties:
                        nullable: true
                    - type: string
                stacking_rule:
                  type: string
                  enum:
                    - exclusive
                    - stackable
                applies_to:
                  type: string
                  enum:
                    - subscription_only
                    - one_time_only
                    - both
                recurring:
                  type: boolean
                max_redemptions:
                  type: integer
                  nullable: true
                  minimum: 1
                starts_at:
                  type: string
                  nullable: true
                  format: date-time
                ends_at:
                  type: string
                  nullable: true
                  format: date-time
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  store_hash:
                    type: string
                  promotion:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      store_hash:
                        type: string
                      name:
                        type: string
                      code:
                        type: string
                        nullable: true
                      application_window:
                        type: string
                        enum:
                          - first_cycle_only
                          - first_n_cycles
                          - all_cycles
                          - tiered_cycles
                          - event_triggered
                      config:
                        type: object
                        additionalProperties:
                          nullable: true
                      eligibility_rules:
                        type: object
                        additionalProperties:
                          nullable: true
                      discount_type:
                        type: string
                        enum:
                          - percent
                          - fixed_cents
                          - free_shipping
                          - gift_item
                      stacking_rule:
                        type: string
                        enum:
                          - exclusive
                          - stackable
                      lock_policy:
                        type: string
                      applies_to:
                        type: string
                        enum:
                          - subscription_only
                          - one_time_only
                          - both
                      recurring:
                        type: boolean
                      status:
                        type: string
                        enum:
                          - active
                          - paused
                          - archived
                      max_redemptions:
                        type: integer
                        nullable: true
                      current_redemptions:
                        type: integer
                      starts_at:
                        type: string
                        nullable: true
                        format: date-time
                      ends_at:
                        type: string
                        nullable: true
                        format: date-time
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
                      archived_at:
                        type: string
                        nullable: true
                        format: date-time
                    required:
                      - id
                      - store_hash
                      - name
                      - code
                      - application_window
                      - config
                      - eligibility_rules
                      - discount_type
                      - stacking_rule
                      - lock_policy
                      - applies_to
                      - recurring
                      - status
                      - max_redemptions
                      - current_redemptions
                      - starts_at
                      - ends_at
                      - created_at
                      - updated_at
                      - archived_at
                required:
                  - store_hash
                  - promotion
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (immutable-field change / bad enum)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    delete:
      tags:
        - admin
        - promotions
      summary: Delete (archive) a subscription promotion
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  store_hash:
                    type: string
                  ok:
                    type: boolean
                    enum:
                      - true
                  id:
                    type: string
                    format: uuid
                required:
                  - store_hash
                  - ok
                  - id
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/email-templates:
    get:
      tags:
        - admin
        - notifications
      summary: List merchant email templates
      description: BRD US-23.3. Lists the store’s configured email templates
        (overrides + defaults).
      security: *a1
      responses:
        "200":
          description: Template list
          content:
            application/json:
              schema:
                type: object
                properties:
                  templates:
                    type: array
                    items:
                      type: object
                      properties:
                        template_key:
                          type: string
                        format:
                          type: string
                          enum:
                            - markdown
                            - mjml
                        subject:
                          type: string
                        is_default:
                          type: integer
                          description: 1 = system default, 0 = merchant override
                        variables_used:
                          type: string
                          nullable: true
                          description: JSON-encoded string[] of variable names
                        updated_at:
                          type: string
                          format: date-time
                      required:
                        - template_key
                        - format
                        - subject
                        - is_default
                        - variables_used
                        - updated_at
                required:
                  - templates
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/email-templates/preview:
    post:
      tags:
        - admin
        - notifications
      summary: Render an email template preview (no DB write)
      description: BRD US-23.3. Renders the supplied template source against sample
        variables and returns the rendered subject + HTML + plain-text. Does not
        persist anything.
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                body_source:
                  type: string
                format:
                  type: string
                  enum:
                    - markdown
                    - mjml
                sample_variables:
                  type: object
                  additionalProperties:
                    nullable: true
              required:
                - subject
                - body_source
                - format
      responses:
        "200":
          description: Rendered preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  subject:
                    type: string
                  body_html:
                    type: string
                  body_text:
                    type: string
                required:
                  - subject
                  - body_html
                  - body_text
        "400":
          description: Invalid body (missing fields / bad format)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Template render error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/email-templates/{key}:
    get:
      tags:
        - admin
        - notifications
      summary: Get one email template by key
      description: BRD US-23.3. {key} is the template_key (lowercase alphanumeric +
        underscores), not a UUID.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: key
          in: path
      responses:
        "200":
          description: Template row
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  store_hash:
                    type: string
                  template_key:
                    type: string
                  format:
                    type: string
                    enum:
                      - markdown
                      - mjml
                  subject:
                    type: string
                  body_source:
                    type: string
                  is_default:
                    type: integer
                  variables_used:
                    type: string
                    nullable: true
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
                required:
                  - id
                  - store_hash
                  - template_key
                  - format
                  - subject
                  - body_source
                  - is_default
                  - variables_used
                  - created_at
                  - updated_at
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - admin
        - notifications
      summary: Upsert an email template
      description: BRD US-23.3. Creates or replaces the template for {key}. Variable
        names are extracted from the subject + body and returned as
        variables_used. {key} must be lowercase alphanumeric + underscores.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: key
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                body_source:
                  type: string
                format:
                  type: string
                  enum:
                    - markdown
                    - mjml
              required:
                - subject
                - body_source
                - format
      responses:
        "200":
          description: Saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - saved
                  template_key:
                    type: string
                  variables_used:
                    type: array
                    items:
                      type: string
                required:
                  - status
                  - template_key
                  - variables_used
        "400":
          description: Invalid body (missing fields / bad format / bad key)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/email-domains:
    get:
      tags:
        - admin
        - notifications
      summary: List custom sending domains
      description: BRD US-23.9. Lists the store’s Resend-provisioned sending domains
        and verification state.
      security: *a1
      responses:
        "200":
          description: Domain list
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      type: object
                      properties:
                        domain:
                          type: string
                        resend_domain_id:
                          type: string
                          nullable: true
                        dkim_record_name:
                          type: string
                          nullable: true
                        dkim_record:
                          type: string
                          nullable: true
                        verification_status:
                          type: string
                          enum:
                            - pending
                            - verified
                            - failed
                        dmarc_aligned:
                          type: integer
                          nullable: true
                        last_verification_attempt_at:
                          type: string
                          nullable: true
                          format: date-time
                        verified_at:
                          type: string
                          nullable: true
                          format: date-time
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - domain
                        - resend_domain_id
                        - dkim_record_name
                        - dkim_record
                        - verification_status
                        - dmarc_aligned
                        - last_verification_attempt_at
                        - verified_at
                        - created_at
                required:
                  - domains
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    post:
      tags:
        - admin
        - notifications
      summary: Provision a custom sending domain
      description: BRD US-23.9. Registers a sending domain with Resend and returns the
        DKIM record the merchant must add to DNS. `domain` must be a valid
        hostname. Verification starts pending; call the verify endpoint after
        adding DNS records.
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                domain:
                  type: string
              required:
                - domain
      responses:
        "200":
          description: Provisioned — DNS records to add
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    type: string
                  dkim_record_name:
                    type: string
                  dkim_record_value:
                    type: string
                  spf_record:
                    type: string
                    nullable: true
                  dmarc_note:
                    type: string
                  verification_status:
                    type: string
                    enum:
                      - pending
                required:
                  - domain
                  - dkim_record_name
                  - dkim_record_value
                  - spf_record
                  - dmarc_note
                  - verification_status
        "400":
          description: Invalid body (domain required / not a valid hostname)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: Resend provisioning failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                required:
                  - error
  /api/v1/admin/email-domains/{domain}/verify:
    post:
      tags:
        - admin
        - notifications
      summary: Re-check sending-domain verification
      description: BRD US-23.9. Triggers a Resend verification re-check for an
        already-provisioned domain and persists the new status. {domain} is the
        hostname (URL-encoded).
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: domain
          in: path
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                type: object
                properties:
                  verified:
                    type: boolean
                  reason:
                    type: string
                required:
                  - verified
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: Resend verify call failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                required:
                  - error
  /api/v1/admin/cs/impersonate:
    post:
      tags:
        - admin
        - cs-tools
      summary: Mint a read-only portal session for a subscriber (CS impersonation)
      description: Epic-22 US-22.3. CS rep clicks "View as subscriber" — mints a
        short-lived (15-minute) read-only portal session stored in
        cs_tool_sessions and returns a portal URL the rep can open in a new tab.
        Cancelled subscriptions cannot be impersonated.
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subscription_id:
                  type: string
                  format: uuid
              required:
                - subscription_id
      responses:
        "201":
          description: Read-only impersonation session minted
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id:
                    type: string
                    format: uuid
                  expires_at:
                    type: string
                    format: date-time
                  portal_url:
                    type: string
                    format: uri
                  read_only:
                    type: boolean
                    enum:
                      - true
                required:
                  - session_id
                  - expires_at
                  - portal_url
                  - read_only
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: Subscription not impersonatable (e.g. cancelled)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/impersonate/sessions/{id}:
    get:
      tags:
        - admin
        - cs-tools
      summary: List active CS impersonation sessions for a subscription
      description: Epic-22 US-22.3. Returns up to 20 active (non-expired, unused) CS
        impersonation sessions for the subscription, most recent first. {id} is
        the subscription_id.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Active impersonation sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        operator_id:
                          type: integer
                        expires_at:
                          type: string
                          format: date-time
                        used_at:
                          type: string
                          nullable: true
                          format: date-time
                        ip_addr:
                          type: string
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - operator_id
                        - expires_at
                        - used_at
                        - ip_addr
                        - created_at
                required:
                  - sessions
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/subscriptions/{id}/force-charge:
    post:
      tags:
        - admin
        - cs-tools
      summary: Force an immediate charge attempt outside the cron schedule
      description: Epic-22 US-22.5. Enqueues a pending charge
        (chain_position=force_manual) scheduled at NOW so the scheduler picks it
        up on the next tick. `reason` is mandatory for the audit trail. Optional
        `amount_cents_override` overrides the plan amount. Returns 409 if the
        subscription is not chargeable, has no payment method, or already has a
        pending force-charge (idempotency guard).
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 500
                amount_cents_override:
                  type: integer
                  minimum: 0
                  exclusiveMinimum: true
              required:
                - reason
      responses:
        "201":
          description: Force-charge queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  charge_id:
                    type: string
                    format: uuid
                  amount_cents:
                    type: integer
                  currency:
                    type: string
                  status:
                    type: string
                    enum:
                      - pending
                  message:
                    type: string
                required:
                  - ok
                  - charge_id
                  - amount_cents
                  - currency
                  - status
                  - message
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Not chargeable, no payment method, or a pending force-charge exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (reason required, amount_cents_override invalid)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/subscriptions/{id}/force-refund:
    post:
      tags:
        - admin
        - cs-tools
      summary: Force an immediate refund on the latest succeeded charge
      description: Epic-22 US-22.5. Refunds the most recent succeeded, un-refunded
        charge for the subscription via the processor adapter. `reason` is
        mandatory. Optional `amount_cents` issues a partial refund (must not
        exceed the charge amount). Returns 409 when no refundable charge exists;
        502 when the processor refund call fails.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 500
                amount_cents:
                  type: integer
                  minimum: 0
                  exclusiveMinimum: true
              required:
                - reason
      responses:
        "200":
          description: Refund issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  charge_id:
                    type: string
                    format: uuid
                  refund_transaction_id:
                    type: string
                  refunded_amount_cents:
                    type: integer
                  reason:
                    type: string
                required:
                  - ok
                  - charge_id
                  - refund_transaction_id
                  - refunded_amount_cents
                  - reason
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: No refundable charge / missing processor transaction / no active
            processor connection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (reason required, amount_cents invalid or exceeds
            charge)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "502":
          description: Processor refund failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/allotments/{id}:
    get:
      tags:
        - admin
        - cs-tools
      summary: Get allotment grant detail with debit history
      description: Epic-22 US-22.7. Returns the grant balance, refresh cadence,
        rollover policy, status, revocation/suspension metadata, and the most
        recent 100 debits. {id} is the grant_id.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Allotment grant detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  customer_id:
                    type: string
                    format: uuid
                  org_id:
                    type: string
                    nullable: true
                  unit_type:
                    type: string
                    enum:
                      - count
                      - dollars
                  amount_per_period:
                    type: number
                  current_balance:
                    type: number
                  refresh_cadence:
                    type: string
                  rollover_policy:
                    type: string
                  rollover_cap:
                    type: number
                    nullable: true
                  next_refresh_at:
                    type: string
                    format: date-time
                  expires_at:
                    type: string
                    nullable: true
                    format: date-time
                  status:
                    type: string
                  revoked_at:
                    type: string
                    nullable: true
                    format: date-time
                  revoked_by:
                    type: string
                    nullable: true
                  revoke_reason:
                    type: string
                    nullable: true
                  suspended_at:
                    type: string
                    nullable: true
                    format: date-time
                  suspended_by:
                    type: string
                    nullable: true
                  created_at:
                    type: string
                    format: date-time
                  debit_history:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        amount:
                          type: number
                        reference_type:
                          type: string
                          enum:
                            - bc_order
                            - charge
                            - manual
                        reference_id:
                          type: string
                        debited_at:
                          type: string
                          format: date-time
                      required:
                        - id
                        - amount
                        - reference_type
                        - reference_id
                        - debited_at
                required:
                  - id
                  - customer_id
                  - org_id
                  - unit_type
                  - amount_per_period
                  - current_balance
                  - refresh_cadence
                  - rollover_policy
                  - rollover_cap
                  - next_refresh_at
                  - expires_at
                  - status
                  - revoked_at
                  - revoked_by
                  - revoke_reason
                  - suspended_at
                  - suspended_by
                  - created_at
                  - debit_history
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/allotments/{id}/revoke:
    post:
      tags:
        - admin
        - cs-tools
      summary: Revoke an allotment grant
      description: Epic-22 US-22.7. Sets status=revoked, records revoked_by +
        revoke_reason, and stops further debits. `reason` is mandatory. Returns
        409 if the grant is already revoked.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 500
              required:
                - reason
      responses:
        "200":
          description: Grant revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  grant_id:
                    type: string
                  status:
                    type: string
                    enum:
                      - revoked
                  reason:
                    type: string
                required:
                  - ok
                  - grant_id
                  - status
                  - reason
        "400":
          description: Invalid body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Grant already revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (reason required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/subscriptions/{id}/custom-fields:
    get:
      tags:
        - admin
        - cs-tools
      summary: List subscription custom-field definitions and current values
      description: Epic-22 US-22.8. Returns every in-scope custom-field definition
        (active and archived) with its current value. Archived fields are
        flagged read-only. {id} is the subscription_id.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Custom-field definitions with current values
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription_id:
                    type: string
                    format: uuid
                  fields:
                    type: array
                    items:
                      type: object
                      properties:
                        field_key:
                          type: string
                        field_label:
                          type: string
                        field_type:
                          type: string
                        required:
                          type: boolean
                        validation_regex:
                          type: string
                          nullable: true
                        options:
                          type: array
                          nullable: true
                          items:
                            type: string
                        subscriber_visibility:
                          type: string
                        display_order:
                          type: integer
                        is_archived:
                          type: boolean
                        current_value:
                          nullable: true
                      required:
                        - field_key
                        - field_label
                        - field_type
                        - required
                        - validation_regex
                        - options
                        - subscriber_visibility
                        - display_order
                        - is_archived
                required:
                  - subscription_id
                  - fields
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    patch:
      tags:
        - admin
        - cs-tools
      summary: Update subscription custom-field values
      description: Epic-22 US-22.8. Partial-update of custom-field values keyed by
        field_key. Writes to archived fields are rejected; values are validated
        against each definition (type/regex/required). The value diff is
        recorded in the audit log. {id} is the subscription_id.
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                fields:
                  type: object
                  additionalProperties:
                    nullable: true
                  description: field_key → value map
              required:
                - fields
      responses:
        "200":
          description: Fields updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  subscription_id:
                    type: string
                    format: uuid
                  updated_fields:
                    type: array
                    items:
                      type: string
                required:
                  - ok
                  - subscription_id
                  - updated_fields
        "400":
          description: Invalid body (fields missing or not an object)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error (archived-field write or value validation failure)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/bulk-create/preview:
    post:
      tags:
        - admin
        - cs-tools
      summary: Stage and validate a CSV for bulk subscription creation
      security: *a1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                csv_data:
                  type: string
              required:
                - csv_data
      responses:
        "200":
          description: Staged preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                    format: uuid
                  valid_rows:
                    type: integer
                  error_rows:
                    type: integer
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        row:
                          type: integer
                        message:
                          type: string
                      required:
                        - row
                        - message
                required:
                  - job_id
                  - valid_rows
                  - error_rows
                  - errors
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/bulk-create/{job_id}/confirm:
    post:
      tags:
        - admin
        - cs-tools
      summary: Confirm a staged bulk-create job
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: job_id
          in: path
      responses:
        "200":
          description: Confirmed — creation queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  created:
                    type: integer
                required:
                  - ok
                  - created
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/cs/bulk-create/{job_id}:
    get:
      tags:
        - admin
        - cs-tools
      summary: Fetch bulk-create job status
      security: *a1
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: job_id
          in: path
      responses:
        "200":
          description: Job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - pending
                      - processing
                      - complete
                      - failed
                  created:
                    type: integer
                  failed:
                    type: integer
                required:
                  - job_id
                  - status
                  - created
                  - failed
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/store/gateways/compatibility:
    get:
      tags:
        - admin
      summary: Get gateway compatibility matrix (US-2.3)
      description: Returns the static PRD §6.3 compatibility matrix for all payment
        gateways. Each entry is classified as "supported" (MVP), "phase-2"
        (roadmap), or "not-supported". Intended for the install-flow
        processor-detection step (BRD §US-2.3).
      security: *a1
      responses:
        "200":
          description: Gateway compatibility matrix
          content:
            application/json:
              schema:
                type: object
                properties:
                  gateways:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Stable gateway identifier (matches processor_connections.processor)
                        name:
                          type: string
                          description: Human-readable gateway display name
                        status:
                          type: string
                          enum:
                            - supported
                            - phase-2
                            - not-supported
                          description: Subscription support tier for this gateway
                        phase:
                          type: integer
                          minimum: 0
                          exclusiveMinimum: true
                          description: Phase number where support is planned (phase-2 entries only)
                        notes:
                          type: string
                          description: Tooltip text explaining compatibility status or migration path
                      required:
                        - id
                        - name
                        - status
                required:
                  - gateways
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/admin/products/{id}/discount-base:
    get:
      tags:
        - plans
      summary: Get the per-product subscribe-and-save discount base override
      description: "Resolves most-specific-wins at renewal: plan.discount_base → this
        product override → store default → 'effective'. null here means the
        product inherits the store default."
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Per-product discount base (null = inherit store default)
          content:
            application/json:
              schema:
                type: object
                properties:
                  bc_product_id:
                    type: integer
                  discount_base:
                    type: string
                    nullable: true
                    enum:
                      - effective
                      - list
                      - null
                required:
                  - bc_product_id
                  - discount_base
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    put:
      tags:
        - plans
      summary: Set or clear the per-product subscribe-and-save discount base override
      description: Body discount_base 'effective' | 'list' | null (null clears the
        override).
      security: *a1
      parameters:
        - schema:
            type: string
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                discount_base:
                  type: string
                  nullable: true
                  enum:
                    - effective
                    - list
                    - null
              required:
                - discount_base
      responses:
        "200":
          description: Updated per-product discount base
          content:
            application/json:
              schema:
                type: object
                properties:
                  bc_product_id:
                    type: integer
                  discount_base:
                    type: string
                    nullable: true
                    enum:
                      - effective
                      - list
                      - null
                required:
                  - bc_product_id
                  - discount_base
        "400":
          description: Invalid discount_base value
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or invalid JWT
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/auth/request-link:
    post:
      tags:
        - portal
        - auth
      summary: Request a magic-link login email for the subscriber portal
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
              required:
                - email
      responses:
        "200":
          description: Link sent (always 200 to prevent email enumeration)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
  /api/v1/portal/auth/verify:
    get:
      tags:
        - portal
        - auth
      summary: Verify magic-link token and set portal session cookie
      parameters:
        - schema:
            type: string
          required: true
          name: token
          in: query
        - schema:
            type: string
          required: true
          name: store_hash
          in: query
      responses:
        "302":
          description: Redirects to portal home with session cookie set
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions:
    get:
      tags:
        - portal
        - subscriptions
      summary: List the authenticated subscriber's subscriptions
      security: &a2
        - PortalSessionCookie: []
      responses:
        "200":
          description: Subscription list
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Subscription"
                required:
                  - subscriptions
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/charges:
    get:
      tags:
        - portal
        - subscriptions
      summary: List charge history for one of the subscriber's subscriptions
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Charges
          content:
            application/json:
              schema:
                type: object
                properties:
                  charges:
                    type: array
                    items:
                      $ref: "#/components/schemas/Charge"
                required:
                  - charges
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/pause:
    post:
      tags:
        - portal
        - subscriptions
      summary: Pause a subscription
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Paused
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already paused or cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/cancel:
    post:
      tags:
        - portal
        - subscriptions
      summary: Cancel a subscription
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/skip:
    post:
      tags:
        - portal
        - subscriptions
      summary: Skip the next charge
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Skipped
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/swap:
    post:
      tags:
        - portal
        - subscriptions
      summary: Swap variant on a subscription
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                variant_id:
                  type: integer
              required:
                - variant_id
      responses:
        "200":
          description: Swapped
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/reschedule:
    post:
      tags:
        - portal
        - subscriptions
      summary: Reschedule the next charge date
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                next_charge_at:
                  type: string
                  format: date-time
              required:
                - next_charge_at
      responses:
        "200":
          description: Rescheduled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/combine-candidates:
    get:
      tags:
        - portal
        - subscriptions
      summary: List subscriptions eligible to combine deliveries with this one
      description: Same store + customer + shipping address + cadence + currency, with
        a pending charge, not already combined. Combining never changes price
        (ADR-0070).
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Eligible combine candidates
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  subscription_id:
                    type: string
                    format: uuid
                  candidates:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        next_charge_at:
                          type: string
                          format: date-time
                        plan_name:
                          type: string
                        amount_cents:
                          type: integer
                        currency:
                          type: string
                      required:
                        - id
                        - next_charge_at
                        - plan_name
                        - amount_cents
                        - currency
                  price_change:
                    type: string
                    enum:
                      - none
                required:
                  - ok
                  - subscription_id
                  - candidates
                  - price_change
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/combine:
    post:
      tags:
        - portal
        - subscriptions
      summary: Combine deliveries — align the chosen subscriptions to one date (no
        proration)
      description: "Aligns the chosen subscriptions to the earliest of their
        next-charge dates at the SAME price (ADR-0070 §2: per-shipment value is
        unchanged) and links them so they materialize as one BC order."
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                with_subscription_ids:
                  type: array
                  items:
                    type: string
                  minItems: 1
              required:
                - with_subscription_ids
      responses:
        "200":
          description: Combined
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  bundle_id:
                    type: string
                  anchor_date:
                    type: string
                  price_change:
                    type: string
                    enum:
                      - none
                required:
                  - ok
                  - bundle_id
                  - anchor_date
                  - price_change
        "400":
          description: Ineligible (address/cadence/currency mismatch or too few)
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Already combined / not active / no pending charge
  /api/v1/portal/subscriptions/{id}/quantity:
    post:
      tags:
        - portal
        - subscriptions
      summary: Update subscription quantity
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                quantity:
                  type: integer
                  minimum: 1
              required:
                - quantity
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/resume:
    post:
      tags:
        - portal
        - subscriptions
      summary: Resume a paused subscription
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Resumed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Not paused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/discount:
    post:
      tags:
        - portal
        - subscriptions
      summary: Apply a discount to a subscription
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                discount_percent:
                  type: number
                  minimum: 0
                  maximum: 100
              required:
                - discount_percent
      responses:
        "200":
          description: Discount applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/interval:
    post:
      tags:
        - portal
        - subscriptions
      summary: Switch billing interval for a subscription
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                interval:
                  type: string
                  enum:
                    - day
                    - week
                    - month
                    - year
                interval_count:
                  type: integer
                  minimum: 1
              required:
                - interval
                - interval_count
      responses:
        "200":
          description: Interval updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/payment-method:
    put:
      tags:
        - portal
        - subscriptions
      summary: Update payment method for a single subscription
      security: *a2
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                payment_method_nonce:
                  type: string
              required:
                - payment_method_nonce
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/customer/payment-method-all:
    put:
      tags:
        - portal
        - subscriptions
      summary: Update payment method across all subscriptions for the customer
      security: *a2
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                payment_method_nonce:
                  type: string
              required:
                - payment_method_nonce
      responses:
        "200":
          description: Updated across all subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  updated_count:
                    type: integer
                required:
                  - ok
                  - updated_count
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/stored-instruments:
    get:
      tags:
        - portal
      summary: List the subscriber's vaulted payment instruments
      security: *a2
      responses:
        "200":
          description: Stored instruments
          content:
            application/json:
              schema:
                type: object
                properties:
                  instruments:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        type:
                          type: string
                        brand:
                          type: string
                        last4:
                          type: string
                        expiry:
                          type: string
                        is_active:
                          type: boolean
                      required:
                        - id
                        - type
                        - is_active
                required:
                  - instruments
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/stored-instruments/iat:
    post:
      tags:
        - portal
      summary: Mint a hosted add-payment-method IAT URL (ADR-0037)
      security: *a2
      responses:
        "200":
          description: IAT URL for hosted add-PM flow
          content:
            application/json:
              schema:
                type: object
                properties:
                  iat_url:
                    type: string
                    format: uri
                required:
                  - iat_url
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/dsr/export:
    get:
      tags:
        - portal
        - dsr
      summary: Export the subscriber's personal data (GDPR Art-15 / Art-20)
      security: *a2
      parameters:
        - schema:
            type: string
            enum:
              - json
              - csv
            description: Export format. Defaults to json; csv returns a text/csv attachment.
          required: false
          description: Export format. Defaults to json; csv returns a text/csv attachment.
          name: format
          in: query
      responses:
        "200":
          description: The subscriber's full data graph (customer, subscriptions, charges,
            consent). JSON by default; text/csv attachment when format=csv.
          content:
            application/json:
              schema:
                type: object
                properties: {}
                additionalProperties:
                  nullable: true
                description: Subscriber data-export graph (buildDsrExport output)
            text/csv:
              schema:
                type: string
                description: CSV serialization of the export
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/dsr/erase:
    post:
      tags:
        - portal
        - dsr
      summary: Request erasure of the subscriber's personal data (GDPR Art-17)
      security: *a2
      responses:
        "200":
          description: Erasure accepted (idempotent — already-erased customers also return
            ok). PII is anonymized within the store retention window.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  customer_id:
                    type: string
                    format: uuid
                  already_erased:
                    type: boolean
                  message:
                    type: string
                required:
                  - ok
                  - customer_id
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/dsr/consent:
    post:
      tags:
        - portal
        - dsr
      summary: Update the subscriber's consent / opt-out flags (GDPR Art-21)
      security: *a2
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                marketing_opt_out:
                  type: boolean
                lifecycle_email_opt_out:
                  type: boolean
                profiling_opt_out:
                  type: boolean
              description: At least one consent flag must be provided
      responses:
        "200":
          description: Consent flags updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  customer_id:
                    type: string
                    format: uuid
                required:
                  - ok
                  - customer_id
        "400":
          description: No consent flags provided, or the request body was not valid JSON
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}:
    get:
      tags:
        - portal
        - subscriptions
      summary: Get one of the authenticated subscriber's subscriptions (with enrichment)
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      responses:
        "200":
          description: Subscription detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/charges/upcoming:
    get:
      tags:
        - portal
        - subscriptions
      summary: List the next scheduled charges for a subscription
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      responses:
        "200":
          description: Upcoming (pending) charges, soonest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  charges:
                    type: array
                    items:
                      $ref: "#/components/schemas/Charge"
                required:
                  - charges
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/reactivate:
    post:
      tags:
        - portal
        - subscriptions
      summary: Reactivate a cancelled subscription (within the grace window)
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      responses:
        "200":
          description: Subscription reactivated (status → active)
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Cannot reactivate — not in cancelled state, grace window expired,
            or the plan is no longer active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/term-confirm:
    post:
      tags:
        - portal
        - subscriptions
      summary: Confirm a pending fixed-term renewal decision
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      responses:
        "200":
          description: Renewal decision confirmed
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Cannot confirm — subscription not active/past_due, no term_end_at,
            or no pending renewal decision
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/shipping-address:
    patch:
      tags:
        - portal
        - subscriptions
      summary: Update a subscription's shipping address
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PortalAddress"
      responses:
        "200":
          description: Shipping address updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  subscription_id:
                    type: string
                    format: uuid
                  shipping_address:
                    $ref: "#/components/schemas/PortalAddress"
                required:
                  - ok
                  - subscription_id
                  - shipping_address
        "400":
          description: Invalid or missing address fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "412":
          description: Address country is not in the plan's country allowlist (US-19.3 /
            US-7.3)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/billing-address:
    patch:
      tags:
        - portal
        - subscriptions
      summary: Update a subscription's billing address
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PortalAddress"
      responses:
        "200":
          description: Billing address updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  subscription_id:
                    type: string
                    format: uuid
                  billing_address:
                    $ref: "#/components/schemas/PortalAddress"
                required:
                  - ok
                  - subscription_id
                  - billing_address
        "400":
          description: Invalid or missing address fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/subscriptions/{id}/bundle-opt-in:
    patch:
      tags:
        - portal
        - subscriptions
      summary: Toggle delivery-bundle opt-in for a subscription (Epic-16)
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                bundle_opt_in:
                  type: boolean
              required:
                - bundle_opt_in
      responses:
        "200":
          description: Bundle opt-in updated
        "400":
          description: bundle_opt_in must be a boolean
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Conflict — action not allowed in the subscription/plan current
            state (typed reason in envelope)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/auth/exchange-bc-session:
    post:
      tags:
        - portal
        - auth
      summary: Exchange a BigCommerce storefront handoff token for a portal session
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                handoff_token:
                  type: string
                  description: Signed BC→portal handoff JWT
              required:
                - handoff_token
      responses:
        "200":
          description: Portal session established (Set-Cookie)
        "400":
          description: Malformed body or missing required fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid handoff token, or the customer could not be resolved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Exchange intentionally disabled — storefront falls back to magic-link
  /api/v1/portal/auth/demo:
    get:
      tags:
        - portal
        - auth
      summary: Mint a demo portal session for a fixture customer (public sandbox only)
      parameters:
        - schema:
            type: string
            enum:
              - alice
              - bob
              - carol
            description: Demo customer key
          required: true
          description: Demo customer key
          name: key
          in: query
      responses:
        "200":
          description: Demo portal session established (Set-Cookie)
        "400":
          description: Missing or unknown demo key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: Demo seed not present in this environment
  /api/v1/portal/stored-instruments/select/{id}:
    put:
      tags:
        - portal
      summary: Select a stored instrument as a subscription's active payment method
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                instrument_token:
                  type: string
                brand:
                  type: string
                last_4:
                  type: string
                  minLength: 4
                  maxLength: 4
              required:
                - instrument_token
                - brand
                - last_4
      responses:
        "200":
          description: Active payment method updated
        "400":
          description: Missing subscription_id or invalid instrument fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/b2b/subscriptions:
    get:
      tags:
        - portal
        - subscriptions
      summary: List the authenticated B2B subscriber's subscriptions
      security: *a2
      responses:
        "200":
          description: B2B subscription list with cycle progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        status:
                          type: string
                          enum:
                            - active
                            - paused
                            - past_due
                            - cancelled
                            - trialing
                            - pending_start
                            - on_hold_oos
                        cycles_completed:
                          type: integer
                      required:
                        - id
                        - status
                        - cycles_completed
                      additionalProperties:
                        nullable: true
                required:
                  - subscriptions
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/b2b/subscriptions/{id}/change-requests:
    post:
      tags:
        - portal
        - subscriptions
      summary: Submit a B2B change request against a subscription (requires approval)
      security: *a2
      parameters:
        - schema:
            type: string
            description: Subscription id
          required: true
          description: Subscription id
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                change_type:
                  type: string
                  enum:
                    - pause
                    - cancel
                    - change_interval
                    - change_quantity
                    - update_payment
                change_payload:
                  type: object
                  additionalProperties:
                    nullable: true
                  description: Proposed new values; shape varies by change_type
              required:
                - change_type
                - change_payload
      responses:
        "200":
          description: Change request submitted (pending approval)
        "400":
          description: Invalid change_type or payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: view_only B2B role cannot submit change requests
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Subscription not found or not owned by this subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/portal/resubscribe:
    post:
      tags:
        - portal
        - subscriptions
      summary: Resubscribe the authenticated customer's eligible lapsed subscription
      security: *a2
      responses:
        "200":
          description: Resubscribe accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthenticated — missing or expired portal session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: Forbidden — session not permitted for this action
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
