openapi: 3.1.0
info:
  title: Subby API
  version: v1
  summary: Subscription OS API for plans, subscribers, renewals, recovery and lifecycle events.
  description: |
    The Subby API is the interface to a Subscription OS powered by a subscription engine.
    Use it to create plans, customers and subscriptions, start checkout, recover failed
    charges with retries and nudges, and receive signed webhook events.

    Subby is not a payment engine. Supported payment rails collect charges. Subby
    coordinates the subscription lifecycle around those charges.

    **Metering.** Only successful (2xx) `POST`, `PATCH` and `DELETE` requests in live count toward the monthly write allowance.
    `GET` requests, sandbox requests, failed requests and idempotent replays are never metered.
    Each operation carries `x-subby-billable` so the docs site and tests can show and verify this.

    **Amounts** are integers in kobo unless an operation explicitly says whole naira. Save to Buy amounts use whole naira. **Timestamps** are ISO 8601 UTC.
  contact:
    name: Subby Developer Support
    email: developers@mysubbyapp.com
    url: https://docs.mysubbyapp.com
servers:
  - url: https://api.mysubbyapp.com/v1
    description: Live
  - url: https://sandbox-api.mysubbyapp.com/v1
    description: Sandbox
security:
  - bearerAuth: []
tags:
  - name: Account & usage
    description: Your Subby account, API usage and Subby invoices.
  - name: Plans
    description: What you sell and how often you charge.
  - name: Customers
    description: The people or businesses you bill.
  - name: Subscriptions
    description: A customer's recurring billing on a plan.
  - name: Charges
    description: Individual payment attempts.
  - name: Checkout sessions
    description: Subby-hosted pages that collect payment details.
  - name: Nudges
    description: Reminders delivered over SMS, WhatsApp or email.
  - name: Webhook endpoints
    description: URLs that receive signed events.
  - name: Events
    description: Things that happened in your account.
  - name: Save to Buy
    description: Instalment purchase plans for eligible e-commerce merchants and their buyers.
  - name: Providers
    description: Provider profile and business-type settings.
  - name: Sandbox test helpers
    description: Sandbox-only tools. Return 404 in live.

x-header-sets:
  standard: &standard_headers
    X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
    RateLimit-Limit: { $ref: '#/components/headers/RateLimit-Limit' }
    RateLimit-Remaining: { $ref: '#/components/headers/RateLimit-Remaining' }
    RateLimit-Reset: { $ref: '#/components/headers/RateLimit-Reset' }
  billable: &billable_headers
    X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
    X-Subby-Billable: { $ref: '#/components/headers/X-Subby-Billable' }
    X-Subby-Write-Usage: { $ref: '#/components/headers/X-Subby-Write-Usage' }
    X-Subby-Write-Limit: { $ref: '#/components/headers/X-Subby-Write-Limit' }
    X-Subby-Usage-Period-End: { $ref: '#/components/headers/X-Subby-Usage-Period-End' }
    Idempotent-Replayed: { $ref: '#/components/headers/Idempotent-Replayed' }
    RateLimit-Limit: { $ref: '#/components/headers/RateLimit-Limit' }
    RateLimit-Remaining: { $ref: '#/components/headers/RateLimit-Remaining' }
    RateLimit-Reset: { $ref: '#/components/headers/RateLimit-Reset' }

paths:
  # ───────────────────────── Account & usage ─────────────────────────
  /account:
    get:
      tags: [Account & usage]
      operationId: getAccount
      summary: Retrieve your account
      x-subby-billable: false
      x-subby-scope: null
      responses:
        '200':
          description: Your account.
          headers: *standard_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /usage:
    get:
      tags: [Account & usage]
      operationId: getUsage
      summary: Retrieve current period usage
      description: Write requests, read requests and nudges for the current billing period. Updates in real time.
      x-subby-billable: false
      x-subby-scope: usage:read
      responses:
        '200':
          description: Usage for the current period.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Usage' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /usage/history:
    get:
      tags: [Account & usage]
      operationId: listUsageHistory
      summary: List usage for past periods
      x-subby-billable: false
      x-subby-scope: usage:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
      responses:
        '200':
          description: Past periods, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Usage' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /account/invoices:
    get:
      tags: [Account & usage]
      operationId: listAccountInvoices
      summary: List your Subby invoices
      x-subby-billable: false
      x-subby-scope: usage:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { $ref: '#/components/parameters/EndingBefore' }
        - name: status
          in: query
          schema: { type: string, enum: [open, paid, overdue, void] }
      responses:
        '200':
          description: Invoices, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/AccountInvoice' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /account/invoices/{id}:
    get:
      tags: [Account & usage]
      operationId: getAccountInvoice
      summary: Retrieve a Subby invoice
      x-subby-billable: false
      x-subby-scope: usage:read
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      responses:
        '200':
          description: The invoice.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/AccountInvoice' }
        '404': { $ref: '#/components/responses/NotFound' }

  /webhook-ips:
    get:
      tags: [Account & usage]
      operationId: listWebhookIps
      summary: List webhook source IP addresses
      security: []
      x-subby-billable: false
      x-subby-scope: null
      responses:
        '200':
          description: IP addresses Subby sends webhooks from.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      ipv4: { type: array, items: { type: string } }
                      updated_at: { type: string, format: date-time }

  # ───────────────────────── Plans ─────────────────────────
  /plans:
    post:
      tags: [Plans]
      operationId: createPlan
      summary: Create a plan
      x-subby-billable: true
      x-subby-scope: plans:write
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PlanCreate' }
            example:
              name: Pro Monthly
              amount: 500000
              currency: NGN
              billing_cycle: monthly
              retry_logic: { enabled: true, max_attempts: 3, retry_intervals_days: [1, 3, 7], escalation_on_failure: pause_service, cancel_after_days_unpaid: 30 }
              reminders: { enabled: true, channels: [whatsapp, sms, email], pre_due_days: [3], post_due_days: [1, 3] }
      responses:
        '201':
          description: The created plan.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [Plans]
      operationId: listPlans
      summary: List plans
      x-subby-billable: false
      x-subby-scope: plans:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { $ref: '#/components/parameters/EndingBefore' }
        - name: status
          in: query
          schema: { type: string, enum: [active, archived] }
      responses:
        '200':
          description: Plans, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Plan' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /plans/{id}:
    parameters:
      - { $ref: '#/components/parameters/PathId' }
    get:
      tags: [Plans]
      operationId: getPlan
      summary: Retrieve a plan
      x-subby-billable: false
      x-subby-scope: plans:read
      responses:
        '200':
          description: The plan.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanResponse' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Plans]
      operationId: updatePlan
      summary: Update a plan
      description: Changing `amount` affects new subscriptions only. Existing subscriptions keep their price.
      x-subby-billable: true
      x-subby-scope: plans:write
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PlanUpdate' }
      responses:
        '200':
          description: The updated plan.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Plans]
      operationId: archivePlan
      summary: Archive a plan
      description: Archived plans can't be used for new subscriptions. Existing subscriptions continue.
      x-subby-billable: true
      x-subby-scope: plans:write
      responses:
        '200':
          description: The archived plan.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanResponse' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Customers ─────────────────────────
  /customers:
    post:
      tags: [Customers]
      operationId: createCustomer
      summary: Create a customer
      x-subby-billable: true
      x-subby-scope: customers:write
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomerCreate' }
            example: { name: Ada Okafor, email: ada@example.com, phone: '+2348000000001', metadata: { your_user_id: u_1024 } }
      responses:
        '201':
          description: The created customer.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomerResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [Customers]
      operationId: listCustomers
      summary: List customers
      x-subby-billable: false
      x-subby-scope: customers:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { $ref: '#/components/parameters/EndingBefore' }
        - { name: email, in: query, schema: { type: string, format: email } }
        - { name: phone, in: query, schema: { type: string } }
        - name: metadata
          in: query
          style: deepObject
          explode: true
          description: Filter by one metadata key, e.g. `metadata[your_user_id]=u_1024`.
          schema: { type: object, additionalProperties: { type: string } }
      responses:
        '200':
          description: Customers, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Customer' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /customers/{id}:
    parameters:
      - { $ref: '#/components/parameters/PathId' }
    get:
      tags: [Customers]
      operationId: getCustomer
      summary: Retrieve a customer
      x-subby-billable: false
      x-subby-scope: customers:read
      parameters:
        - { $ref: '#/components/parameters/Expand' }
      responses:
        '200':
          description: The customer.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomerResponse' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Customers]
      operationId: updateCustomer
      summary: Update a customer
      x-subby-billable: true
      x-subby-scope: customers:write
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomerUpdate' }
      responses:
        '200':
          description: The updated customer.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomerResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Customers]
      operationId: deleteCustomer
      summary: Delete a customer
      description: Cancels the customer's active subscriptions immediately, then deletes the customer.
      x-subby-billable: true
      x-subby-scope: customers:write
      responses:
        '200':
          description: Deletion confirmation.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      object: { const: customer }
                      deleted: { const: true }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Subscriptions ─────────────────────────
  /subscriptions:
    post:
      tags: [Subscriptions]
      operationId: createSubscription
      summary: Create a subscription
      description: |
        Creates a subscription for a customer who already has a payment method. To collect a payment method
        at the same time, use a checkout session in `subscription` mode instead.
      x-subby-billable: true
      x-subby-scope: subscriptions:write
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionCreate' }
      responses:
        '201':
          description: The created subscription.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [Subscriptions]
      operationId: listSubscriptions
      summary: List subscriptions
      x-subby-billable: false
      x-subby-scope: subscriptions:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { $ref: '#/components/parameters/EndingBefore' }
        - { $ref: '#/components/parameters/Expand' }
        - { name: customer, in: query, schema: { type: string } }
        - { name: plan, in: query, schema: { type: string } }
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/SubscriptionStatus' }
      responses:
        '200':
          description: Subscriptions, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Subscription' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /subscriptions/{id}:
    parameters:
      - { $ref: '#/components/parameters/PathId' }
    get:
      tags: [Subscriptions]
      operationId: getSubscription
      summary: Retrieve a subscription
      x-subby-billable: false
      x-subby-scope: subscriptions:read
      parameters:
        - { $ref: '#/components/parameters/Expand' }
      responses:
        '200':
          description: The subscription.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionResponse' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Subscriptions]
      operationId: updateSubscription
      summary: Update a subscription
      description: Change plan, quantity, payment method, metadata, or undo a scheduled cancellation.
      x-subby-billable: true
      x-subby-scope: subscriptions:write
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionUpdate' }
      responses:
        '200':
          description: The updated subscription.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /subscriptions/{id}/pause:
    post:
      tags: [Subscriptions]
      operationId: pauseSubscription
      summary: Pause a subscription
      x-subby-billable: true
      x-subby-scope: subscriptions:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                resumes_at: { type: string, format: date-time, description: Resume automatically at this time. Omit to pause until resumed. }
                reason: { type: string, maxLength: 200 }
      responses:
        '200':
          description: The paused subscription.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionResponse' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /subscriptions/{id}/resume:
    post:
      tags: [Subscriptions]
      operationId: resumeSubscription
      summary: Resume a paused subscription
      x-subby-billable: true
      x-subby-scope: subscriptions:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      responses:
        '200':
          description: The resumed subscription.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionResponse' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /subscriptions/{id}/cancel:
    post:
      tags: [Subscriptions]
      operationId: cancelSubscription
      summary: Cancel a subscription
      x-subby-billable: true
      x-subby-scope: subscriptions:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                at_period_end: { type: boolean, default: true }
                reason: { type: string, enum: [customer_request, merchant_request, other], default: merchant_request }
      responses:
        '200':
          description: The cancelled (or scheduled to cancel) subscription.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionResponse' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /subscriptions/{id}/unit-records:
    post:
      tags: [Subscriptions]
      operationId: createUnitRecord
      summary: Report units for a per-unit plan
      description: For plans with `overage_per_unit` (for example litres of diesel or laundry items). Units are billed at the next renewal.
      x-subby-billable: true
      x-subby-scope: subscriptions:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [quantity]
              properties:
                quantity: { type: integer, minimum: 0 }
                action: { type: string, enum: [increment, set], default: increment }
                timestamp: { type: string, format: date-time }
                description: { type: string, maxLength: 200 }
      responses:
        '201':
          description: The unit record.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/UnitRecord' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  # ───────────────────────── Charges ─────────────────────────
  /charges:
    post:
      tags: [Charges]
      operationId: createCharge
      summary: Create a one-off charge
      description: Charges a customer's default payment method once, outside their subscription schedule.
      x-subby-billable: true
      x-subby-scope: charges:write
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer, amount, currency]
              properties:
                customer: { type: string }
                amount: { type: integer, minimum: 100, description: Kobo. }
                currency: { type: string, enum: [NGN] }
                description: { type: string, maxLength: 200 }
                payment_method: { type: string, description: Defaults to the customer's default payment method. }
                metadata: { $ref: '#/components/schemas/Metadata' }
      responses:
        '201':
          description: The charge. A declined payment still returns 201 with `status` `failed`.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChargeResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '422': { $ref: '#/components/responses/Unprocessable' }
    get:
      tags: [Charges]
      operationId: listCharges
      summary: List charges
      x-subby-billable: false
      x-subby-scope: charges:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { $ref: '#/components/parameters/EndingBefore' }
        - { name: customer, in: query, schema: { type: string } }
        - { name: subscription, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string, enum: [pending, succeeded, failed] } }
      responses:
        '200':
          description: Charges, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Charge' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /charges/{id}:
    get:
      tags: [Charges]
      operationId: getCharge
      summary: Retrieve a charge
      x-subby-billable: false
      x-subby-scope: charges:read
      parameters:
        - { $ref: '#/components/parameters/PathId' }
        - { $ref: '#/components/parameters/Expand' }
      responses:
        '200':
          description: The charge.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChargeResponse' }
        '404': { $ref: '#/components/responses/NotFound' }

  /charges/{id}/retry:
    post:
      tags: [Charges]
      operationId: retryCharge
      summary: Retry a failed charge now
      description: Use when a customer asks to pay immediately. Automatic retries don't need this and are free.
      x-subby-billable: true
      x-subby-scope: charges:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                payment_method: { type: string }
      responses:
        '201':
          description: The new charge attempt.
          headers: *billable_headers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChargeResponse' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  # ───────────────────────── Checkout sessions ─────────────────────────
  /checkout-sessions:
    post:
      tags: [Checkout sessions]
      operationId: createCheckoutSession
      summary: Create a checkout session
      x-subby-billable: true
      x-subby-scope: checkout_sessions:write
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CheckoutSessionCreate' }
      responses:
        '201':
          description: The session. Redirect the customer to `url`.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/CheckoutSession' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /checkout-sessions/{id}:
    get:
      tags: [Checkout sessions]
      operationId: getCheckoutSession
      summary: Retrieve a checkout session
      x-subby-billable: false
      x-subby-scope: checkout_sessions:read
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      responses:
        '200':
          description: The session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/CheckoutSession' }
        '404': { $ref: '#/components/responses/NotFound' }

  /checkout-sessions/{id}/expire:
    post:
      tags: [Checkout sessions]
      operationId: expireCheckoutSession
      summary: Expire a checkout session
      x-subby-billable: true
      x-subby-scope: checkout_sessions:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      responses:
        '200':
          description: The expired session.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/CheckoutSession' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  # ───────────────────────── Nudges ─────────────────────────
  /nudges:
    post:
      tags: [Nudges]
      operationId: createNudge
      summary: Send a nudge
      description: |
        Sends a reminder to a customer now. Costs 1 write request plus 1 nudge from your allowance once sent.
        Limited to 3 manual nudges per customer per 24 hours.
      x-subby-billable: true
      x-subby-consumes-nudge: true
      x-subby-scope: nudges:write
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NudgeCreate' }
      responses:
        '201':
          description: The queued nudge.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Nudge' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '422': { $ref: '#/components/responses/Unprocessable' }
    get:
      tags: [Nudges]
      operationId: listNudges
      summary: List nudges
      x-subby-billable: false
      x-subby-scope: nudges:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { $ref: '#/components/parameters/EndingBefore' }
        - { name: customer, in: query, schema: { type: string } }
        - { name: subscription, in: query, schema: { type: string } }
        - { name: channel, in: query, schema: { type: string, enum: [sms, whatsapp, email] } }
        - { name: status, in: query, schema: { type: string, enum: [queued, sent, delivered, failed] } }
      responses:
        '200':
          description: Nudges, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Nudge' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /nudges/{id}:
    get:
      tags: [Nudges]
      operationId: getNudge
      summary: Retrieve a nudge
      x-subby-billable: false
      x-subby-scope: nudges:read
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      responses:
        '200':
          description: The nudge.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Nudge' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Webhook endpoints ─────────────────────────
  /webhook-endpoints:
    post:
      tags: [Webhook endpoints]
      operationId: createWebhookEndpoint
      summary: Create a webhook endpoint
      x-subby-billable: true
      x-subby-scope: webhooks:write
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, enabled_events]
              properties:
                url: { type: string, format: uri }
                description: { type: string, maxLength: 200 }
                enabled_events: { type: array, minItems: 1, items: { type: string }, example: ['subscription.*', charge.failed] }
      responses:
        '201':
          description: The endpoint, including `secret` (shown only in this response).
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    allOf:
                      - { $ref: '#/components/schemas/WebhookEndpoint' }
                      - type: object
                        properties:
                          secret: { type: string, example: whsec_4f9c... }
        '400': { $ref: '#/components/responses/BadRequest' }
    get:
      tags: [Webhook endpoints]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      x-subby-billable: false
      x-subby-scope: webhooks:read
      responses:
        '200':
          description: Endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/WebhookEndpoint' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /webhook-endpoints/{id}:
    parameters:
      - { $ref: '#/components/parameters/PathId' }
    get:
      tags: [Webhook endpoints]
      operationId: getWebhookEndpoint
      summary: Retrieve a webhook endpoint
      x-subby-billable: false
      x-subby-scope: webhooks:read
      responses:
        '200':
          description: The endpoint.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/WebhookEndpoint' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Webhook endpoints]
      operationId: updateWebhookEndpoint
      summary: Update a webhook endpoint
      x-subby-billable: true
      x-subby-scope: webhooks:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                description: { type: string }
                enabled_events: { type: array, items: { type: string } }
                status: { type: string, enum: [enabled, disabled] }
      responses:
        '200':
          description: The updated endpoint.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/WebhookEndpoint' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Webhook endpoints]
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      x-subby-billable: true
      x-subby-scope: webhooks:write
      responses:
        '200':
          description: Deletion confirmation.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      object: { const: webhook_endpoint }
                      deleted: { const: true }
        '404': { $ref: '#/components/responses/NotFound' }

  /webhook-endpoints/{id}/test:
    post:
      tags: [Webhook endpoints]
      operationId: testWebhookEndpoint
      summary: Send a test event
      x-subby-billable: true
      x-subby-scope: webhooks:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      responses:
        '200':
          description: Delivery result.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      event: { type: string }
                      response_status: { type: [integer, 'null'] }
                      duration_ms: { type: integer }
                      delivered: { type: boolean }

  /webhook-endpoints/{id}/roll-secret:
    post:
      tags: [Webhook endpoints]
      operationId: rollWebhookSecret
      summary: Roll the signing secret
      description: The old secret keeps signing alongside the new one for 24 hours.
      x-subby-billable: true
      x-subby-scope: webhooks:write
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      responses:
        '200':
          description: The endpoint with the new `secret`.
          headers: *billable_headers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    allOf:
                      - { $ref: '#/components/schemas/WebhookEndpoint' }
                      - type: object
                        properties:
                          secret: { type: string }
                          previous_secret_expires_at: { type: string, format: date-time }

  # ───────────────────────── Events ─────────────────────────
  /events:
    get:
      tags: [Events]
      operationId: listEvents
      summary: List events
      description: Events from the last 30 days, newest first.
      x-subby-billable: false
      x-subby-scope: webhooks:read
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { $ref: '#/components/parameters/EndingBefore' }
        - { name: type, in: query, description: Exact type or wildcard such as `subscription.*`., schema: { type: string } }
        - { name: 'created[gte]', in: query, schema: { type: string, format: date-time } }
        - { name: 'created[lte]', in: query, schema: { type: string, format: date-time } }
      responses:
        '200':
          description: Events.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Event' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /events/{id}:
    get:
      tags: [Events]
      operationId: getEvent
      summary: Retrieve an event
      x-subby-billable: false
      x-subby-scope: webhooks:read
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      responses:
        '200':
          description: The event.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Event' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── Sandbox test helpers ─────────────────────────
  /test/clocks:
    post:
      tags: [Sandbox test helpers]
      operationId: createTestClock
      summary: Create a test clock
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [frozen_time]
              properties:
                name: { type: string, maxLength: 100 }
                frozen_time: { type: string, format: date-time }
      responses:
        '201':
          description: The test clock.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/TestClock' }
        '404': { $ref: '#/components/responses/NotFound' }
    get:
      tags: [Sandbox test helpers]
      operationId: listTestClocks
      summary: List test clocks
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      responses:
        '200':
          description: Test clocks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/TestClock' } }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /test/clocks/{id}:
    parameters:
      - { $ref: '#/components/parameters/PathId' }
    get:
      tags: [Sandbox test helpers]
      operationId: getTestClock
      summary: Retrieve a test clock
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      responses:
        '200':
          description: The test clock.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/TestClock' }
    delete:
      tags: [Sandbox test helpers]
      operationId: deleteTestClock
      summary: Delete a test clock and its customers
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      responses:
        '200':
          description: Deletion confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      deleted: { const: true }

  /test/clocks/{id}/advance:
    post:
      tags: [Sandbox test helpers]
      operationId: advanceTestClock
      summary: Advance a test clock
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      parameters:
        - { $ref: '#/components/parameters/PathId' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [frozen_time]
              properties:
                frozen_time: { type: string, format: date-time, description: Must be later than the current frozen_time and at most 1 year ahead. }
      responses:
        '202':
          description: Advancing started. Status is `advancing` until `test_clock.ready`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/TestClock' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /test/events/trigger:
    post:
      tags: [Sandbox test helpers]
      operationId: triggerTestEvent
      summary: Trigger a webhook event
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type: { type: string, example: subscription.escalated }
                object: { type: string, description: Existing sandbox object ID to use instead of generated data. }
                overrides: { type: object, additionalProperties: true }
      responses:
        '201':
          description: The created event, queued for delivery.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/Event' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /test/nudge-outbox:
    get:
      tags: [Sandbox test helpers]
      operationId: listNudgeOutbox
      summary: List simulated nudges with rendered messages
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/StartingAfter' }
        - { name: customer, in: query, schema: { type: string } }
      responses:
        '200':
          description: Outbox entries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: array
                    items:
                      allOf:
                        - { $ref: '#/components/schemas/Nudge' }
                        - type: object
                          properties:
                            to: { type: string }
                            rendered_message: { type: string }
                            payment_link: { type: string, format: uri }
                  meta: { $ref: '#/components/schemas/ListMeta' }

  /test/reset:
    post:
      tags: [Sandbox test helpers]
      operationId: resetSandbox
      summary: Delete all sandbox data
      description: Keeps API keys and webhook endpoints.
      x-subby-billable: false
      x-subby-sandbox-only: true
      x-subby-scope: null
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirm]
              properties:
                confirm: { const: RESET }
      responses:
        '202':
          description: Reset started.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      status: { const: resetting }

  # ───────────────────────── Save to Buy (staging) ─────────────────────────
  /provider-savings:
    get:
      tags: [Save to Buy]
      operationId: listProviderSavingsPlans
      summary: List Save to Buy plans
      description: Lists buyer plans for the authenticated e-commerce merchant. Returns 404 when Save to Buy is unavailable for the account.
      x-subby-release-status: staging
      x-subby-billable: false
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [pending, active, overdue, completed, shipped, delivered, cancelled] }
        - name: familyId
          in: query
          schema: { type: string }
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { description: A page of Save to Buy plans., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsPlanListResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /provider-savings/summary:
    get:
      tags: [Save to Buy]
      operationId: getProviderSavingsSummary
      summary: Retrieve Save to Buy dashboard metrics
      x-subby-release-status: staging
      x-subby-billable: false
      parameters:
        - name: period
          in: query
          schema: { type: string, enum: [weekly, monthly], default: weekly }
      responses:
        '200': { description: Save to Buy dashboard metrics., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsSummaryResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /provider-savings/products:
    get:
      tags: [Save to Buy]
      operationId: listSaveToBuyProducts
      summary: List Save to Buy products
      x-subby-release-status: staging
      x-subby-billable: false
      responses:
        '200': { description: Save to Buy products and sales metrics., content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/SaveToBuyProduct' } } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /provider-savings/share/{familyId}:
    get:
      tags: [Save to Buy]
      operationId: getSaveToBuyShareLinks
      summary: Retrieve product sharing links
      x-subby-release-status: staging
      x-subby-billable: false
      parameters:
        - name: familyId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Hosted link and embed snippets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      url: { type: string, format: uri }
                      buttonHtml: { type: string }
                      iframeHtml: { type: string }
        '404': { $ref: '#/components/responses/NotFound' }

  /provider-savings/{id}:
    get:
      tags: [Save to Buy]
      operationId: getProviderSavingsPlan
      summary: Retrieve a Save to Buy plan
      x-subby-release-status: staging
      x-subby-billable: false
      parameters: [{ $ref: '#/components/parameters/PathId' }]
      responses:
        '200': { description: The buyer's Save to Buy plan., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsPlanResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /provider-savings/{id}/extend:
    post:
      tags: [Save to Buy]
      operationId: extendSavingsPlan
      summary: Extend a Save to Buy plan
      description: Moves unpaid instalments and the deadline. Only active or overdue plans can be extended.
      x-subby-release-status: staging
      x-subby-billable: true
      parameters: [{ $ref: '#/components/parameters/PathId' }]
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [days], properties: { days: { type: integer, minimum: 1, maximum: 60 } } } } }
      responses:
        '200': { description: Extended plan and the buyer's extension fee., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsPlanResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /provider-savings/{id}/ship:
    post:
      tags: [Save to Buy]
      operationId: markSavingsPlanShipped
      summary: Mark a paid product shipped
      x-subby-release-status: staging
      x-subby-billable: true
      parameters: [{ $ref: '#/components/parameters/PathId' }]
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/FulfilmentNote' } } } }
      responses:
        '200': { description: The updated plan., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsPlanResponse' } } } }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /provider-savings/{id}/deliver:
    post:
      tags: [Save to Buy]
      operationId: markSavingsPlanDelivered
      summary: Mark a paid product delivered
      x-subby-release-status: staging
      x-subby-billable: true
      parameters: [{ $ref: '#/components/parameters/PathId' }]
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/FulfilmentNote' } } } }
      responses:
        '200': { description: The delivered plan. Sends savings_plan.fulfilled., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsPlanResponse' } } } }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /provider-savings/{id}/cancel:
    post:
      tags: [Save to Buy]
      operationId: cancelProviderSavingsPlan
      summary: Cancel a Save to Buy plan
      x-subby-release-status: staging
      x-subby-billable: true
      parameters: [{ $ref: '#/components/parameters/PathId' }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { reason: { type: string, maxLength: 250 } } } } } }
      responses:
        '200': { description: The cancelled plan. Any limited slot is released., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsPlanResponse' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /public/save-to-buy/plans/{planId}:
    get:
      tags: [Save to Buy]
      operationId: getPublicSaveToBuyProduct
      summary: Retrieve public Save to Buy terms
      security: []
      x-subby-release-status: staging
      x-subby-public-operation: true
      x-subby-billable: false
      parameters:
        - name: planId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Product terms and available instalment options., content: { application/json: { schema: { $ref: '#/components/schemas/PublicSaveToBuyProductResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /public/save-to-buy/plans/{planId}/start:
    post:
      tags: [Save to Buy]
      operationId: startPublicSavingsPlan
      summary: Start a Save to Buy plan
      security: []
      x-subby-release-status: staging
      x-subby-public-operation: true
      x-subby-billable: false
      parameters:
        - name: planId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/StartSavingsPlanRequest' } } }
      responses:
        '201': { description: Plan created with the first payment link., content: { application/json: { schema: { $ref: '#/components/schemas/StartSavingsPlanResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /public/save-to-buy/p/{token}:
    get:
      tags: [Save to Buy]
      operationId: getPrivateSavingsPlan
      summary: Retrieve a buyer's private plan
      description: The token is secret. Treat this URL like a password-reset link.
      security: []
      x-subby-release-status: staging
      x-subby-billable: false
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The buyer's private plan., content: { application/json: { schema: { $ref: '#/components/schemas/SavingsPlanResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /public/save-to-buy/p/{token}/pay:
    post:
      tags: [Save to Buy]
      operationId: createSavingsPaymentLink
      summary: Create a Save to Buy payment link
      security: []
      x-subby-release-status: staging
      x-subby-public-operation: true
      x-subby-billable: false
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                instalments: { type: integer, minimum: 1 }
                payBalance: { type: boolean }
                amount: { type: integer, minimum: 200, description: Whole naira; flexible plans only. }
      responses:
        '200': { description: Payment link., content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: object, properties: { paymentUrl: { type: string, format: uri }, amountDue: { type: integer } } } } } } } }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /public/save-to-buy/p/{token}/pay-fee:
    post:
      tags: [Save to Buy]
      operationId: createSavingsFeePaymentLink
      summary: Create an extension-fee payment link
      security: []
      x-subby-release-status: staging
      x-subby-public-operation: true
      x-subby-billable: false
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Extension-fee payment link. }
        '429': { $ref: '#/components/responses/RateLimited' }

  /public/save-to-buy/p/{token}/cancel:
    post:
      tags: [Save to Buy]
      operationId: cancelPublicSavingsPlan
      summary: Cancel a buyer's Save to Buy plan
      security: []
      x-subby-release-status: staging
      x-subby-public-operation: true
      x-subby-billable: false
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The cancelled plan and released slot. }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ───────────────────────── Providers (staging) ─────────────────────────
  /providers/me/vertical:
    patch:
      tags: [Providers]
      operationId: updateMyProviderVertical
      summary: Set the provider business type
      description: Available while approval is pending, or once for an approved provider with no business type. An approved provider with an existing type receives 403 unless sending the current value.
      x-subby-release-status: staging
      x-subby-billable: true
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [vertical], properties: { vertical: { $ref: '#/components/schemas/ProviderVertical' } } } } }
      responses:
        '200': { description: Business type saved or unchanged. }
        '403': { $ref: '#/components/responses/Forbidden' }

  /providers/{id}/vertical:
    patch:
      tags: [Providers]
      operationId: updateProviderVerticalAsAdmin
      summary: Change a provider business type as an administrator
      description: Records the previous and new types, timestamp, administrator and reason in the provider history.
      x-subby-release-status: staging
      x-subby-billable: true
      parameters: [{ $ref: '#/components/parameters/PathId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vertical, reason]
              properties:
                vertical: { $ref: '#/components/schemas/ProviderVertical' }
                reason: { type: string, minLength: 5, maxLength: 500 }
      responses:
        '200': { description: Business type changed and recorded. }
        '403': { $ref: '#/components/responses/Forbidden' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 'sk_test_ / sk_live_ / rk_test_ / rk_live_'

  headers:
    X-Request-Id: { description: Unique request ID., schema: { type: string } }
    X-Subby-Billable: { description: Whether this request counted toward the write allowance., schema: { type: string, enum: ['true', 'false'] } }
    X-Subby-Write-Usage: { description: Write requests counted this period., schema: { type: integer } }
    X-Subby-Write-Limit: { description: "Included write requests, or `unlimited`.", schema: { type: string } }
    X-Subby-Usage-Period-End: { description: End of the current billing period (UTC)., schema: { type: string, format: date-time } }
    Idempotent-Replayed: { description: Present and `true` when the response is a replay., schema: { type: string } }
    RateLimit-Limit: { description: Requests allowed per minute., schema: { type: integer } }
    RateLimit-Remaining: { description: Requests left in the current window., schema: { type: integer } }
    RateLimit-Reset: { description: Seconds until the window resets., schema: { type: integer } }
    Retry-After: { description: Seconds to wait before retrying., schema: { type: integer } }
  parameters:
    PathId:
      name: id
      in: path
      required: true
      schema: { type: string, maxLength: 64 }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    StartingAfter:
      name: starting_after
      in: query
      schema: { type: string }
    EndingBefore:
      name: ending_before
      in: query
      schema: { type: string }
    Expand:
      name: 'expand[]'
      in: query
      description: Related fields to expand, up to 4.
      schema: { type: array, maxItems: 4, items: { type: string } }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Makes retries safe. Replays return the original response and are not metered.
      schema: { type: string, maxLength: 255 }

  responses:
    BadRequest:
      description: Invalid request.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    Unauthorized:
      description: Missing, invalid, revoked or wrong-environment key.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    Forbidden:
      description: Insufficient scope, or live access not enabled.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    NotFound:
      description: Object or route not found in this environment.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    Conflict:
      description: Idempotency conflict.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    Unprocessable:
      description: Valid request that conflicts with the object's state.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    RateLimited:
      description: Too many requests.
      headers:
        Retry-After: { $ref: '#/components/headers/Retry-After' }
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }

  schemas:
    Metadata:
      type: object
      maxProperties: 20
      propertyNames: { maxLength: 40 }
      additionalProperties: { type: string, maxLength: 500 }

    ListMeta:
      type: object
      required: [has_more, limit]
      properties:
        has_more: { type: boolean }
        next_cursor: { type: [string, 'null'] }
        limit: { type: integer }

    Error:
      type: object
      required: [type, code, message, request_id]
      properties:
        type: { type: string, enum: [authentication_error, permission_error, invalid_request_error, state_error, idempotency_error, rate_limit_error, api_error] }
        code: { type: string }
        message: { type: string }
        param: { type: [string, 'null'] }
        request_id: { type: string }
        doc_url: { type: string, format: uri }

    ErrorEnvelope:
      type: object
      required: [success, error]
      properties:
        success: { const: false }
        error: { $ref: '#/components/schemas/Error' }

    Account:
      type: object
      properties:
        id: { type: string, example: acct_2Xm9 }
        object: { const: account }
        business_name: { type: string }
        api_plan: { type: string, enum: [starter, growth, scale, enterprise] }
        live_access: { type: string, enum: [not_requested, pending_review, approved, rejected, suspended] }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    AccountResponse:
      type: object
      properties:
        success: { const: true }
        data: { $ref: '#/components/schemas/Account' }

    Usage:
      type: object
      properties:
        object: { const: usage }
        livemode: { type: boolean }
        plan: { type: string, enum: [starter, growth, scale, enterprise] }
        period:
          type: object
          properties:
            start: { type: string, format: date-time }
            end: { type: string, format: date-time }
        write_requests:
          type: object
          properties:
            used: { type: integer }
            included: { type: [integer, 'null'], description: null means unlimited. }
            remaining: { type: [integer, 'null'] }
            overage: { type: integer }
            overage_rate_per_1000: { type: [integer, 'null'], description: "Kobo per 1,000 write requests." }
            projected_overage_amount: { type: integer, description: Kobo. Final amount is on your invoice. }
        read_requests:
          type: object
          properties:
            used: { type: integer }
            metered: { const: false }
        nudges:
          type: object
          properties:
            used: { type: integer }
            included: { type: [integer, 'null'] }
            overage: { type: integer }
            overage_rate: { type: [integer, 'null'], description: Kobo per nudge. }
            overage_amount: { type: integer }
            overage_cap: { type: [integer, 'null'] }
        updated_at: { type: string, format: date-time }

    AccountInvoice:
      type: object
      properties:
        id: { type: string, example: inv_2026_09_8k1 }
        object: { const: invoice }
        status: { type: string, enum: [open, paid, overdue, void] }
        period_start: { type: string, format: date-time }
        period_end: { type: string, format: date-time }
        currency: { const: NGN }
        lines:
          type: array
          items:
            type: object
            properties:
              type: { type: string, enum: [plan_fee, write_request_overage, nudge_overage, proration, credit] }
              description: { type: string }
              quantity: { type: integer }
              unit_amount: { type: integer }
              amount: { type: integer }
        total: { type: integer }
        due_at: { type: string, format: date-time }
        paid_at: { type: [string, 'null'], format: date-time }
        pdf_url: { type: string, format: uri }

    RetryLogic:
      type: object
      properties:
        enabled: { type: boolean, default: true }
        max_attempts: { type: integer, minimum: 0, maximum: 8, default: 3 }
        retry_intervals_days: { type: array, items: { type: integer, minimum: 1, maximum: 30 }, default: [1, 3, 7] }
        escalation_on_failure: { type: string, enum: [pause_service, restrict_access, hold_delivery, none], default: pause_service }
        cancel_after_days_unpaid: { type: [integer, 'null'], minimum: 1, maximum: 180, default: 30 }

    Reminders:
      type: object
      properties:
        enabled: { type: boolean, default: true }
        channels: { type: array, items: { type: string, enum: [whatsapp, sms, email] }, default: [whatsapp, sms, email] }
        pre_due_days: { type: array, items: { type: integer, minimum: 1, maximum: 30 }, default: [3] }
        post_due_days: { type: array, items: { type: integer, minimum: 1, maximum: 60 }, default: [1, 3] }

    PlanCreate:
      type: object
      required: [name, amount, currency, billing_cycle]
      properties:
        name: { type: string, maxLength: 120 }
        description: { type: string, maxLength: 500 }
        amount: { type: integer, minimum: 100, description: Kobo. }
        currency: { type: string, enum: [NGN] }
        billing_cycle: { type: string, enum: [daily, weekly, monthly, quarterly, per_delivery] }
        trial_days: { type: integer, minimum: 0, maximum: 365, default: 0 }
        pause_enabled: { type: boolean, default: false }
        pro_rate: { type: boolean, default: true }
        overage_per_unit: { type: [integer, 'null'], description: Kobo per unit reported with unit records. }
        category: { type: string, description: "Vertical key, e.g. waste_management, diesel_supply, gym_fitness." }
        service_config: { type: object, additionalProperties: true }
        retry_logic: { $ref: '#/components/schemas/RetryLogic' }
        reminders: { $ref: '#/components/schemas/Reminders' }
        metadata: { $ref: '#/components/schemas/Metadata' }

    PlanUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        amount: { type: integer, minimum: 100 }
        trial_days: { type: integer }
        pause_enabled: { type: boolean }
        pro_rate: { type: boolean }
        overage_per_unit: { type: [integer, 'null'] }
        service_config: { type: object, additionalProperties: true }
        retry_logic: { $ref: '#/components/schemas/RetryLogic' }
        reminders: { $ref: '#/components/schemas/Reminders' }
        metadata: { $ref: '#/components/schemas/Metadata' }

    Plan:
      allOf:
        - { $ref: '#/components/schemas/PlanCreate' }
        - type: object
          properties:
            id: { type: string, example: plan_8ZpR1v }
            object: { const: plan }
            status: { type: string, enum: [active, archived] }
            livemode: { type: boolean }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }

    PlanResponse:
      type: object
      properties:
        success: { const: true }
        data: { $ref: '#/components/schemas/Plan' }

    ProviderVertical:
      type: string
      enum: [generic, estate_management, meal_plan, gym_fitness, insurance, waste, fuel, laundry, ecommerce]

    SaveToBuyTerms:
      type: object
      required: [mode, cycleMonths, allowedFrequencies]
      properties:
        mode: { type: string, enum: [fixed, flexible], default: fixed }
        cycleMonths: { type: integer, minimum: 1, maximum: 12 }
        allowedFrequencies:
          type: array
          minItems: 1
          uniqueItems: true
          items: { type: string, enum: [daily, weekly, biweekly, monthly] }
        totalSlots: { type: [integer, 'null'], minimum: 1, description: null means unlimited. }
        productImage: { type: [string, 'null'], format: uri, maxLength: 500 }

    SavingsInstalment:
      type: object
      properties:
        index: { type: integer, minimum: 1 }
        dueAt: { type: string, format: date-time }
        amount: { type: integer, minimum: 200, description: Whole naira. }
        status: { type: string, enum: [scheduled, paid, paid_early, missed] }
        paidAt: { type: [string, 'null'], format: date-time }

    SavingsPlan:
      type: object
      properties:
        id: { type: string, example: SB-7F3K9Q }
        object: { const: savings_plan }
        status: { type: string, enum: [pending, active, overdue, completed, shipped, delivered, cancelled] }
        productName: { type: string }
        productImage: { type: [string, 'null'], format: uri }
        mode: { type: string, enum: [fixed, flexible] }
        frequency: { type: [string, 'null'], enum: [daily, weekly, biweekly, monthly, null] }
        currency: { const: NGN }
        targetAmount: { type: integer, description: Whole naira. }
        amountPaid: { type: integer, description: Whole naira. }
        outstanding: { type: integer, description: Whole naira. }
        instalmentAmount: { type: [integer, 'null'], description: Whole naira. }
        instalmentCount: { type: [integer, 'null'] }
        instalmentsPaid: { type: integer }
        nextDue:
          type: [object, 'null']
          properties:
            index: { type: integer }
            dueAt: { type: string, format: date-time }
            amount: { type: integer, description: Whole naira. }
        deadline: { type: string, format: date-time }
        instalments: { type: array, items: { $ref: '#/components/schemas/SavingsInstalment' } }
        pendingFee: { type: integer, description: Whole naira. }
        completedAt: { type: [string, 'null'], format: date-time }
        shippedAt: { type: [string, 'null'], format: date-time }
        deliveredAt: { type: [string, 'null'], format: date-time }
        refundWindowEndsAt: { type: [string, 'null'], format: date-time }

    SavingsPlanResponse:
      type: object
      properties:
        success: { const: true }
        data: { $ref: '#/components/schemas/SavingsPlan' }

    SavingsPlanListResponse:
      type: object
      properties:
        success: { const: true }
        data:
          type: object
          properties:
            items: { type: array, items: { $ref: '#/components/schemas/SavingsPlan' } }
            total: { type: integer }
            page: { type: integer }
            limit: { type: integer }

    SaveToBuyProduct:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string }
        price: { type: integer, description: Whole naira. }
        isActive: { type: boolean }
        image: { type: [string, 'null'], format: uri }
        cycleMonths: { type: integer }
        mode: { type: string, enum: [fixed, flexible] }
        allowedFrequencies: { type: array, items: { type: string, enum: [daily, weekly, biweekly, monthly] } }
        slots:
          type: object
          properties:
            total: { type: [integer, 'null'] }
            used: { type: integer }
            remaining: { type: [integer, 'null'] }
        views: { type: integer }
        saving: { type: integer }
        overdue: { type: integer }
        readyToDeliver: { type: integer }
        delivered: { type: integer }
        collected: { type: integer, description: Whole naira. }

    SavingsSummaryResponse:
      type: object
      properties:
        success: { const: true }
        data:
          type: object
          additionalProperties: true
          description: Dashboard metrics including series, outstanding balances, fulfilment queues, completion rate and product performance.

    PublicSaveToBuyProductResponse:
      type: object
      properties:
        success: { const: true }
        data:
          type: object
          properties:
            planId: { type: string }
            name: { type: string }
            description: { type: string }
            image: { type: [string, 'null'], format: uri }
            merchant: { type: string }
            currency: { const: NGN }
            targetAmount: { type: integer, description: Whole naira. }
            cycleMonths: { type: integer }
            mode: { type: string, enum: [fixed, flexible] }
            allowedFrequencies: { type: array, items: { type: string, enum: [daily, weekly, biweekly, monthly] } }
            options:
              type: array
              items:
                type: object
                properties:
                  frequency: { type: string }
                  count: { type: integer }
                  amount: { type: integer }
                  finalAmount: { type: integer }
                  total: { type: integer }
                  valid: { type: boolean }
            slots:
              type: object
              properties:
                total: { type: [integer, 'null'] }
                remaining: { type: [integer, 'null'] }
                soldOut: { type: boolean }
            minPayment: { const: 200 }

    StartSavingsPlanRequest:
      type: object
      required: [frequency, name, email, phone]
      properties:
        frequency: { type: string, enum: [daily, weekly, biweekly, monthly] }
        name: { type: string, minLength: 2, maxLength: 100 }
        email: { type: string, format: email }
        phone: { type: string, description: Nigerian phone number in local or +234 format. }
        delivery:
          type: object
          properties:
            address: { type: string, maxLength: 250 }
            city: { type: string, maxLength: 80 }
            state: { type: string, maxLength: 80 }
            note: { type: string, maxLength: 250 }
        amount: { type: integer, minimum: 200, description: Whole naira; flexible plans only. }

    StartSavingsPlanResponse:
      type: object
      properties:
        success: { const: true }
        data:
          type: object
          properties:
            publicId: { type: string, example: SB-7F3K9Q }
            accessToken: { type: string, description: Secret buyer token. Never expose it in merchant systems or logs. }
            pageUrl: { type: string, format: uri }
            paymentUrl: { type: string, format: uri }
            amountDue: { type: integer, description: Whole naira. }

    FulfilmentNote:
      type: object
      properties:
        note: { type: string, maxLength: 250 }

    PaymentMethodSummary:
      type: object
      properties:
        id: { type: string, example: pm_2Hx9 }
        type: { type: string, enum: [card, bank_account, ussd] }
        brand: { type: [string, 'null'], example: verve }
        last4: { type: [string, 'null'] }
        bank_name: { type: [string, 'null'] }
        exp_month: { type: [integer, 'null'] }
        exp_year: { type: [integer, 'null'] }

    CustomerCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 120 }
        email: { type: string, format: email }
        phone: { type: string, description: "E.164, e.g. +2348012345678." }
        metadata: { $ref: '#/components/schemas/Metadata' }
        test_clock: { type: string, description: Sandbox only. }

    CustomerUpdate:
      type: object
      properties:
        name: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        default_payment_method: { type: string, description: "A payment method already attached to this customer, or a `pm_test_` ID in sandbox." }
        metadata: { $ref: '#/components/schemas/Metadata' }

    Customer:
      type: object
      properties:
        id: { type: string, example: cus_4kQ2mT }
        object: { const: customer }
        name: { type: string }
        email: { type: [string, 'null'] }
        phone: { type: [string, 'null'] }
        default_payment_method:
          oneOf:
            - { type: 'null' }
            - { $ref: '#/components/schemas/PaymentMethodSummary' }
        test_clock: { type: [string, 'null'] }
        metadata: { $ref: '#/components/schemas/Metadata' }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    CustomerResponse:
      type: object
      properties:
        success: { const: true }
        data: { $ref: '#/components/schemas/Customer' }

    SubscriptionStatus:
      type: string
      enum: [incomplete, trialing, active, past_due, paused, cancelled]

    SubscriptionCreate:
      type: object
      required: [customer, plan]
      properties:
        customer: { type: string }
        plan: { type: string }
        quantity: { type: integer, minimum: 1, default: 1 }
        payment_method: { type: string, description: Defaults to the customer's default payment method. }
        trial_days: { type: integer, description: Overrides the plan's trial_days. }
        start_at: { type: string, format: date-time, description: Start in the future. Defaults to now. }
        metadata: { $ref: '#/components/schemas/Metadata' }

    SubscriptionUpdate:
      type: object
      properties:
        plan: { type: string }
        quantity: { type: integer, minimum: 1 }
        proration: { type: string, enum: [prorate_now, next_period, none], default: prorate_now }
        payment_method: { type: string }
        cancel_at_period_end: { type: boolean }
        metadata: { $ref: '#/components/schemas/Metadata' }

    Subscription:
      type: object
      properties:
        id: { type: string, example: sub_9fK3mQ2xLp }
        object: { const: subscription }
        status: { $ref: '#/components/schemas/SubscriptionStatus' }
        customer: { type: string, description: "ID, or Customer when expanded." }
        plan: { type: string, description: "ID, or Plan when expanded." }
        quantity: { type: integer }
        current_period_start: { type: string, format: date-time }
        current_period_end: { type: string, format: date-time }
        next_charge_at: { type: [string, 'null'], format: date-time }
        trial_end: { type: [string, 'null'], format: date-time }
        cancel_at_period_end: { type: boolean }
        paused_at: { type: [string, 'null'], format: date-time }
        resumes_at: { type: [string, 'null'], format: date-time }
        cancelled_at: { type: [string, 'null'], format: date-time }
        cancellation_reason: { type: [string, 'null'], enum: [customer_request, merchant_request, nonpayment, checkout_expired, other, null] }
        retry:
          type: [object, 'null']
          properties:
            attempt: { type: integer }
            max_attempts: { type: integer }
            next_retry_at: { type: [string, 'null'], format: date-time }
        escalation:
          type: [object, 'null']
          properties:
            action: { type: string, enum: [pause, restrict, hold, none] }
            triggered_at: { type: string, format: date-time }
        latest_charge: { type: [string, 'null'] }
        metadata: { $ref: '#/components/schemas/Metadata' }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    SubscriptionResponse:
      type: object
      properties:
        success: { const: true }
        data: { $ref: '#/components/schemas/Subscription' }

    UnitRecord:
      type: object
      properties:
        id: { type: string, example: ur_5Tq }
        object: { const: unit_record }
        subscription: { type: string }
        quantity: { type: integer }
        action: { type: string, enum: [increment, set] }
        period_total: { type: integer }
        timestamp: { type: string, format: date-time }

    Charge:
      type: object
      properties:
        id: { type: string, example: chg_3Wd8Lk }
        object: { const: charge }
        amount: { type: integer }
        currency: { const: NGN }
        status: { type: string, enum: [pending, succeeded, failed] }
        customer: { type: string }
        subscription: { type: [string, 'null'] }
        attempt_number: { type: integer }
        will_retry: { type: boolean }
        next_retry_at: { type: [string, 'null'], format: date-time }
        failure_code: { type: [string, 'null'] }
        failure_message: { type: [string, 'null'] }
        payment_method: { $ref: '#/components/schemas/PaymentMethodSummary' }
        processor: { type: string, enum: [paystack, flutterwave, simulator] }
        description: { type: [string, 'null'] }
        paid_at: { type: [string, 'null'], format: date-time }
        metadata: { $ref: '#/components/schemas/Metadata' }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    ChargeResponse:
      type: object
      properties:
        success: { const: true }
        data: { $ref: '#/components/schemas/Charge' }

    CheckoutSessionCreate:
      type: object
      required: [mode, success_url, cancel_url]
      properties:
        mode: { type: string, enum: [subscription, setup], description: '`setup` only collects a payment method.' }
        customer: { type: string, description: "Existing customer. If omitted, a customer is created from checkout details." }
        plan: { type: string, description: Required in subscription mode. }
        quantity: { type: integer, minimum: 1, default: 1 }
        success_url: { type: string, format: uri }
        cancel_url: { type: string, format: uri }
        expires_in_minutes: { type: integer, minimum: 30, maximum: 1440, default: 1440 }
        metadata: { $ref: '#/components/schemas/Metadata' }

    CheckoutSession:
      type: object
      properties:
        id: { type: string, example: cs_7Hn2Qp }
        object: { const: checkout_session }
        mode: { type: string, enum: [subscription, setup] }
        status: { type: string, enum: [open, completed, expired] }
        url: { type: string, format: uri }
        customer: { type: [string, 'null'] }
        plan: { type: [string, 'null'] }
        subscription: { type: [string, 'null'] }
        success_url: { type: string, format: uri }
        cancel_url: { type: string, format: uri }
        expires_at: { type: string, format: date-time }
        metadata: { $ref: '#/components/schemas/Metadata' }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    NudgeCreate:
      type: object
      required: [customer, template]
      properties:
        customer: { type: string }
        subscription: { type: string }
        channels: { type: array, items: { type: string, enum: [whatsapp, sms, email] }, description: In order of preference. Defaults to the plan's reminder channels. }
        template: { type: string, enum: [payment_due, payment_failed, update_payment_method, service_paused, custom] }
        custom_message: { type: string, maxLength: 480, description: Required when template is custom. A payment link is appended automatically. }

    Nudge:
      type: object
      properties:
        id: { type: string, example: ndg_7Yt2Qa }
        object: { const: nudge }
        customer: { type: string }
        subscription: { type: [string, 'null'] }
        channel: { type: string, enum: [whatsapp, sms, email] }
        template: { type: string }
        source: { type: string, enum: [automatic, api, dashboard] }
        status: { type: string, enum: [queued, sent, delivered, failed] }
        failure_reason: { type: [string, 'null'] }
        counts_toward_allowance: { type: boolean }
        billed_as_overage: { type: boolean }
        sent_at: { type: [string, 'null'], format: date-time }
        delivered_at: { type: [string, 'null'], format: date-time }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    WebhookEndpoint:
      type: object
      properties:
        id: { type: string, example: we_1Lp4 }
        object: { const: webhook_endpoint }
        url: { type: string, format: uri }
        description: { type: [string, 'null'] }
        enabled_events: { type: array, items: { type: string } }
        status: { type: string, enum: [enabled, disabled] }
        disabled_reason: { type: [string, 'null'] }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    Event:
      type: object
      properties:
        id: { type: string, example: evt_3Lk9qPz1Tx }
        object: { const: event }
        type: { type: string }
        api_version: { type: string }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }
        data:
          type: object
          properties:
            object: { type: object, additionalProperties: true }
            previous_attributes: { type: object, additionalProperties: true }

    TestClock:
      type: object
      properties:
        id: { type: string, example: clock_1Qz }
        object: { const: test_clock }
        name: { type: [string, 'null'] }
        frozen_time: { type: string, format: date-time }
        status: { type: string, enum: [ready, advancing, internal_failure] }
        customer_count: { type: integer }
        deletes_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
