openapi: 3.1.0
info:
  title: MoPay Developer Billing API
  version: 2026-10-09
  description: >-
    Project-scoped fixed recurring card subscriptions. Commands require an
    Idempotency-Key. Never expose project API keys or webhook signing secrets
    in browser code. Customer email is optional at creation but required for
    hosted subscription checkout.
servers:
  - url: https://mopay.co.ls/api/v1
    description: Production API base; use the matching project environment.
security:
  - ProjectBearer: []
tags:
  - name: Catalog
  - name: Customers
  - name: Checkout
  - name: Subscriptions
  - name: Invoices
  - name: Webhooks
  - name: Policy
  - name: Customer portal
paths:
  /products:
    get:
      tags: [Catalog]
      summary: List products
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses: {'200': {$ref: '#/components/responses/ProductPage'}, '401': {$ref: '#/components/responses/Error'}, '429': {$ref: '#/components/responses/Error'}}
    post:
      tags: [Catalog]
      summary: Create a product
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {$ref: '#/components/requestBodies/Product'}
      responses: {'201': {$ref: '#/components/responses/Product'}, '200': {$ref: '#/components/responses/Product'}, '400': {$ref: '#/components/responses/Error'}, '409': {$ref: '#/components/responses/Error'}}
  /products/{productId}:
    parameters: [{$ref: '#/components/parameters/ProductId'}]
    get:
      tags: [Catalog]
      summary: Get a product
      responses: {'200': {$ref: '#/components/responses/Product'}, '404': {$ref: '#/components/responses/Error'}}
    patch:
      tags: [Catalog]
      summary: Archive a product
      description: Archiving prevents the product and its prices from being sold in new checkout sessions; historical invoices remain.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {active: {const: false}}, required: [active], additionalProperties: false}}}}
      responses: {'200': {$ref: '#/components/responses/Product'}, '404': {$ref: '#/components/responses/Error'}}
  /prices:
    get:
      tags: [Catalog]
      summary: List immutable prices
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses: {'200': {$ref: '#/components/responses/PricePage'}, '401': {$ref: '#/components/responses/Error'}}
    post:
      tags: [Catalog]
      summary: Create a fixed recurring price
      description: Amount is in integer minor units. Current supported currency is ZAR; subscription quantity is one.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {$ref: '#/components/requestBodies/Price'}
      responses: {'201': {$ref: '#/components/responses/Price'}, '200': {$ref: '#/components/responses/Price'}, '400': {$ref: '#/components/responses/Error'}}
  /prices/{priceId}:
    parameters: [{$ref: '#/components/parameters/PriceId'}]
    get:
      tags: [Catalog]
      summary: Get a price
      responses: {'200': {$ref: '#/components/responses/Price'}, '404': {$ref: '#/components/responses/Error'}}
    patch:
      tags: [Catalog]
      summary: Archive a price
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {active: {const: false}}, required: [active], additionalProperties: false}}}}
      responses: {'200': {$ref: '#/components/responses/Price'}, '404': {$ref: '#/components/responses/Error'}}
  /customers:
    get:
      tags: [Customers]
      summary: List customers
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses: {'200': {$ref: '#/components/responses/CustomerPage'}}
    post:
      tags: [Customers]
      summary: Create a customer
      description: external_id, email, name and metadata are individually optional. A valid email is required when starting hosted subscription checkout.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {$ref: '#/components/requestBodies/Customer'}
      responses: {'201': {$ref: '#/components/responses/Customer'}, '200': {$ref: '#/components/responses/Customer'}, '400': {$ref: '#/components/responses/Error'}}
  /customers/{customerId}:
    parameters: [{$ref: '#/components/parameters/CustomerId'}]
    get:
      tags: [Customers]
      summary: Get a customer
      responses: {'200': {$ref: '#/components/responses/Customer'}, '404': {$ref: '#/components/responses/Error'}}
    patch:
      tags: [Customers]
      summary: Update customer details
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {$ref: '#/components/requestBodies/CustomerPatch'}
      responses: {'200': {$ref: '#/components/responses/Customer'}, '400': {$ref: '#/components/responses/Error'}, '404': {$ref: '#/components/responses/Error'}}
  /checkout/sessions:
    post:
      tags: [Checkout]
      summary: Create hosted subscription checkout
      description: Use a stable purchase_reference. Redirect the customer to the returned hosted URL. Checkout completion alone does not establish paid access.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {$ref: '#/components/requestBodies/Checkout'}
      responses: {'201': {$ref: '#/components/responses/CheckoutSession'}, '200': {$ref: '#/components/responses/CheckoutSession'}, '400': {$ref: '#/components/responses/Error'}, '409': {$ref: '#/components/responses/Error'}}
  /checkout/sessions/{sessionId}:
    parameters: [{$ref: '#/components/parameters/SessionId'}]
    get:
      tags: [Checkout]
      summary: Retrieve a checkout session
      responses: {'200': {$ref: '#/components/responses/CheckoutSession'}, '404': {$ref: '#/components/responses/Error'}}
  /checkout/sessions/{sessionId}/expire:
    parameters: [{$ref: '#/components/parameters/SessionId'}]
    post:
      tags: [Checkout]
      summary: Expire an open checkout session
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'200': {$ref: '#/components/responses/CheckoutSession'}, '409': {$ref: '#/components/responses/Error'}}
  /checkout/sessions/{sessionId}/retry:
    parameters: [{$ref: '#/components/parameters/SessionId'}]
    post:
      tags: [Checkout]
      summary: Retry a safely retryable initial checkout
      description: Rejected for submitted or unresolved payment attempts. Never use this to retry an unknown result.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'200': {$ref: '#/components/responses/CheckoutSession'}, '409': {$ref: '#/components/responses/Error'}}
  /subscriptions:
    get:
      tags: [Subscriptions]
      summary: List developer API subscriptions
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses: {'200': {$ref: '#/components/responses/SubscriptionPage'}}
  /subscriptions/{subscriptionId}:
    parameters: [{$ref: '#/components/parameters/SubscriptionId'}]
    get:
      tags: [Subscriptions]
      summary: Retrieve a subscription
      responses: {'200': {$ref: '#/components/responses/Subscription'}, '404': {$ref: '#/components/responses/Error'}}
    patch:
      tags: [Subscriptions]
      summary: Update metadata or cancellation schedule
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/SubscriptionUpdate'}}}}
      responses: {'200': {$ref: '#/components/responses/Subscription'}, '400': {$ref: '#/components/responses/Error'}}
  /subscriptions/{subscriptionId}/preview:
    parameters: [{$ref: '#/components/parameters/SubscriptionId'}]
    post:
      tags: [Subscriptions]
      summary: Preview a price change
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ChangePreviewRequest'}}}}
      responses: {'200': {description: Preview result, content: {application/json: {schema: {$ref: '#/components/schemas/ChangePreview'}}}}, '400': {$ref: '#/components/responses/Error'}, '409': {$ref: '#/components/responses/Error'}}
  /subscriptions/{subscriptionId}/changes:
    parameters: [{$ref: '#/components/parameters/SubscriptionId'}]
    post:
      tags: [Subscriptions]
      summary: Apply a previously returned change quote
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {quote_id: {type: string}}, required: [quote_id], additionalProperties: false}}}}
      responses: {'200': {description: Applied or scheduled subscription change, content: {application/json: {schema: {$ref: '#/components/schemas/SubscriptionChange'}}}}, '409': {$ref: '#/components/responses/Error'}}
  /subscriptions/{subscriptionId}/cancel:
    parameters: [{$ref: '#/components/parameters/SubscriptionId'}]
    post:
      tags: [Subscriptions]
      summary: Cancel a subscription
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {cancel_at_period_end: {type: boolean}, cancellation_reason: {type: string, maxLength: 500}}, additionalProperties: false}}}}
      responses: {'200': {$ref: '#/components/responses/Subscription'}, '409': {$ref: '#/components/responses/Error'}}
  /subscriptions/{subscriptionId}/resume:
    parameters: [{$ref: '#/components/parameters/SubscriptionId'}]
    post:
      tags: [Subscriptions]
      summary: Resume an eligible subscription
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'200': {$ref: '#/components/responses/Subscription'}, '409': {$ref: '#/components/responses/Error'}}
  /subscriptions/{subscriptionId}/pause-collection:
    parameters: [{$ref: '#/components/parameters/SubscriptionId'}]
    post:
      tags: [Subscriptions]
      summary: Pause automatic collection
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/PauseCollection'}}}}
      responses: {'200': {$ref: '#/components/responses/Subscription'}, '409': {$ref: '#/components/responses/Error'}}
  /invoices:
    get:
      tags: [Invoices]
      summary: List invoices newest first
      description: Cursor pagination keeps the same reverse-chronological ordering.
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses: {'200': {$ref: '#/components/responses/InvoicePage'}}
  /invoices/{invoiceId}:
    parameters: [{$ref: '#/components/parameters/InvoiceId'}]
    get:
      tags: [Invoices]
      summary: Retrieve an invoice and its safe payment-intent summary
      responses: {'200': {$ref: '#/components/responses/Invoice'}, '404': {$ref: '#/components/responses/Error'}}
  /billing/policy:
    get:
      tags: [Policy]
      summary: Retrieve project billing defaults and allowed overrides
      responses: {'200': {description: Current project policy, content: {application/json: {schema: {$ref: '#/components/schemas/BillingPolicy'}}}}}
    patch:
      tags: [Policy]
      summary: Update project billing policy
      description: Project owners choose the defaults and per-request options their customers can use.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/BillingPolicy'}}}}
      responses: {'200': {description: Saved policy, content: {application/json: {schema: {$ref: '#/components/schemas/BillingPolicy'}}}}, '400': {$ref: '#/components/responses/Error'}}
  /events:
    get:
      tags: [Webhooks]
      summary: List billing events
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}, {name: type, in: query, schema: {type: string}}]
      responses: {'200': {$ref: '#/components/responses/EventPage'}}
  /events/{eventId}:
    parameters: [{$ref: '#/components/parameters/EventId'}]
    get:
      tags: [Webhooks]
      summary: Retrieve a billing event
      responses: {'200': {$ref: '#/components/responses/Event'}, '404': {$ref: '#/components/responses/Error'}}
    post:
      tags: [Webhooks]
      summary: Replay an event delivery
      description: Replays the existing event ID and does not submit or create a payment.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'200': {$ref: '#/components/responses/Event'}, '404': {$ref: '#/components/responses/Error'}}
  /webhook_endpoints:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses: {'200': {$ref: '#/components/responses/WebhookEndpointPage'}}
    post:
      tags: [Webhooks]
      summary: Register a webhook endpoint
      description: The signing_secret is returned only once. Store it server-side.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {$ref: '#/components/requestBodies/WebhookEndpoint'}
      responses: {'201': {$ref: '#/components/responses/WebhookEndpointWithSecret'}, '200': {$ref: '#/components/responses/WebhookEndpointWithSecret'}, '400': {$ref: '#/components/responses/Error'}}
  /webhook_endpoints/{endpointId}:
    parameters: [{$ref: '#/components/parameters/EndpointId'}]
    get:
      tags: [Webhooks]
      summary: Retrieve a webhook endpoint without its secret
      responses: {'200': {$ref: '#/components/responses/WebhookEndpoint'}, '404': {$ref: '#/components/responses/Error'}}
    patch:
      tags: [Webhooks]
      summary: Update, deactivate or rotate a webhook endpoint
      description: rotate_secret returns a new signing_secret once; during rotation both versions overlap for 24 hours.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {$ref: '#/components/requestBodies/WebhookEndpointUpdate'}
      responses: {'200': {$ref: '#/components/responses/WebhookEndpointWithSecret'}, '400': {$ref: '#/components/responses/Error'}}
    delete:
      tags: [Webhooks]
      summary: Archive a webhook endpoint
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'200': {$ref: '#/components/responses/WebhookEndpoint'}}
  /webhook_endpoints/{endpointId}/test:
    parameters: [{$ref: '#/components/parameters/EndpointId'}]
    post:
      tags: [Webhooks]
      summary: Queue a signed synthetic endpoint test
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'202': {description: Queued webhook test}}
  /billing_portal/sessions:
    post:
      tags: [Customer portal]
      summary: Create a short-lived customer portal session
      description: The bearer URL is revealed once and must not be logged or exposed to other customers.
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {customer: {type: string}, return_url: {type: string, format: uri}}, required: [customer], additionalProperties: false}}}}
      responses: {'201': {description: One-time portal capability, content: {application/json: {schema: {type: object, properties: {url: {type: string, format: uri}, expires_at: {type: string, format: date-time}}, required: [url, expires_at]}}}}, '409': {$ref: '#/components/responses/Error'}}
components:
  securitySchemes:
    ProjectBearer:
      type: http
      scheme: bearer
      description: Project API key; server-to-server only.
  parameters:
    IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, format: uuid}}
    Limit: {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 25}}
    Cursor: {name: cursor, in: query, schema: {type: string, maxLength: 200}}
    ProductId: {name: productId, in: path, required: true, schema: {type: string}}
    PriceId: {name: priceId, in: path, required: true, schema: {type: string}}
    CustomerId: {name: customerId, in: path, required: true, schema: {type: string}}
    SessionId: {name: sessionId, in: path, required: true, schema: {type: string}}
    SubscriptionId: {name: subscriptionId, in: path, required: true, schema: {type: string}}
    InvoiceId: {name: invoiceId, in: path, required: true, schema: {type: string}}
    EventId: {name: eventId, in: path, required: true, schema: {type: string}}
    EndpointId: {name: endpointId, in: path, required: true, schema: {type: string}}
  schemas:
    Error: {type: object, properties: {error: {type: object, properties: {code: {type: string}, message: {type: string}, request_id: {type: string}}, required: [code, message, request_id]}}, required: [error]}
    Page: {type: object, properties: {data: {type: array, items: {}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}
    Product:
      type: object
      properties:
        id: {type: string}
        object: {const: product}
        name: {type: string}
        description: {type: [string, 'null']}
        active: {type: boolean}
        metadata: {type: [object, 'null']}
        archived_at: {type: [string, 'null'], format: date-time}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
      required: [id, object, name, description, active, metadata, archived_at, created_at, updated_at]
    Price:
      type: object
      properties:
        id: {type: string}
        object: {const: price}
        product: {type: string}
        unit_amount: {type: integer}
        currency: {type: string, const: ZAR}
        recurring:
          type: object
          properties:
            interval: {enum: [DAY, WEEK, MONTH, YEAR]}
            interval_count: {type: integer}
          required: [interval, interval_count]
        active: {type: boolean}
        metadata: {type: [object, 'null']}
        archived_at: {type: [string, 'null'], format: date-time}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
      required: [id, object, product, unit_amount, currency, recurring, active, metadata, archived_at, created_at, updated_at]
    Customer: {type: object, properties: {id: {type: string}, object: {const: customer}, external_id: {type: [string, 'null']}, email: {type: [string, 'null'], format: email}, name: {type: [string, 'null']}, metadata: {type: [object, 'null']}, archived_at: {type: [string, 'null'], format: date-time}, created_at: {type: string, format: date-time}, updated_at: {type: string, format: date-time}}, required: [id, object, external_id, email, name, metadata, archived_at, created_at, updated_at]}
    CheckoutSession: {type: object, properties: {id: {type: string}, object: {const: checkout.session}, status: {enum: [open, complete, expired, failed, processing]}, mode: {const: subscription}, customer: {type: string}, price: {type: string}, subscription: {type: [string, 'null']}, invoice: {type: [string, 'null']}, url: {type: string, format: uri}, expires_at: {type: string, format: date-time}}, required: [id, object, status, mode, customer, price, subscription, invoice, url, expires_at]}
    Subscription:
      type: object
      properties:
        id: {type: string}
        object: {const: subscription}
        origin: {type: [string, 'null'], enum: [legacy_import, null], description: Identifies a subscription projected from historical MoPay records.}
        customer: {type: string}
        status: {type: string}
        price: {type: [string, 'null']}
        unit_amount: {type: [integer, 'null']}
        currency: {type: string}
        recurring:
          oneOf:
            - type: object
              properties: {interval: {enum: [DAY, WEEK, MONTH, YEAR]}, interval_count: {type: integer}}
              required: [interval, interval_count]
            - type: object
              properties: {frequency: {type: string}}
              required: [frequency]
        current_period_start: {type: [string, 'null'], format: date-time}
        current_period_end: {type: [string, 'null'], format: date-time}
        current_period_paid: {type: [boolean, 'null']}
        paid_through_at: {type: [string, 'null'], format: date-time}
        next_billing_at: {type: [string, 'null'], format: date-time}
        cancel_at_period_end: {type: boolean}
        pause_collection:
          oneOf:
            - type: 'null'
            - type: object
              properties:
                behavior: {enum: [keep_as_draft, void, mark_uncollectible]}
                resumes_at: {type: [string, 'null'], format: date-time}
              required: [behavior, resumes_at]
        latest_invoice: {type: [string, 'null']}
        revision: {type: integer}
        metadata: {type: [object, 'null']}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
      required: [id, object, origin, customer, status, price, unit_amount, currency, recurring, current_period_start, current_period_end, current_period_paid, paid_through_at, next_billing_at, cancel_at_period_end, pause_collection, latest_invoice, revision, metadata, created_at, updated_at]
    Invoice:
      type: object
      properties:
        id: {type: string}
        object: {const: invoice}
        origin: {type: [string, 'null'], enum: [legacy_import, null], description: Identifies an invoice projected from historical MoPay records.}
        number: {type: string}
        customer: {type: string}
        subscription: {type: string}
        reason: {type: string}
        status: {type: string}
        currency: {type: string}
        subtotal_minor: {type: integer}
        credit_minor: {type: integer}
        total_minor: {type: integer}
        paid_minor: {type: integer}
        due_minor: {type: integer}
        period_start: {type: string, format: date-time}
        period_end: {type: string, format: date-time}
        finalized_at: {type: [string, 'null'], format: date-time}
        paid_at: {type: [string, 'null'], format: date-time}
        revision: {type: integer}
        lines:
          type: array
          items:
            type: object
            properties:
              id: {type: string}
              description: {type: string}
              quantity: {type: integer}
              unit_amount_minor: {type: integer}
              amount_minor: {type: integer}
              currency: {type: string}
              period_start: {type: string, format: date-time}
              period_end: {type: string, format: date-time}
              proration: {type: boolean}
              price_snapshot: {type: [object, 'null']}
            required: [id, description, quantity, unit_amount_minor, amount_minor, currency, period_start, period_end, proration, price_snapshot]
        payment_intents:
          type: array
          items:
            type: object
            properties:
              id: {type: string}
              object: {const: payment_intent}
              status: {type: string}
              amount_minor: {type: integer}
              currency: {type: string}
              revision: {type: integer}
            required: [id, object, status, amount_minor, currency, revision]
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
      required: [id, object, origin, number, customer, subscription, reason, status, currency, subtotal_minor, credit_minor, total_minor, paid_minor, due_minor, period_start, period_end, finalized_at, paid_at, revision, lines, payment_intents, created_at, updated_at]
    BillingEvent:
      type: object
      properties:
        id: {type: string}
        object: {const: event}
        type: {type: string}
        project: {type: string}
        environment: {enum: [SANDBOX, LIVE]}
        resource: {type: string}
        resource_revision: {type: integer}
        api_version: {type: string}
        created: {type: integer, description: Unix timestamp in seconds}
        data: {type: object, properties: {object: {type: object}}, required: [object]}
        deliveries:
          type: array
          items:
            type: object
            properties:
              endpoint_id: {type: string}
              status: {type: string}
              attempt_count: {type: integer}
              created_at: {type: string, format: date-time}
              updated_at: {type: string, format: date-time}
              next_attempt_at: {type: [string, 'null'], format: date-time}
              delivered_at: {type: [string, 'null'], format: date-time}
              last_error: {type: [string, 'null']}
            required: [endpoint_id, status, attempt_count, created_at, updated_at, next_attempt_at, delivered_at, last_error]
      required: [id, object, type, project, environment, resource, resource_revision, api_version, created, data, deliveries]
    WebhookEndpoint: {type: object, properties: {id: {type: string}, object: {const: webhook_endpoint}, url: {type: string, format: uri}, event_types: {type: array, items: {type: string}}, active: {type: boolean}, secret_version: {type: integer}, created_at: {type: string, format: date-time}, updated_at: {type: string, format: date-time}, archived_at: {type: [string, 'null'], format: date-time}}, required: [id, object, url, event_types, active, secret_version, created_at, updated_at, archived_at]}
    SubscriptionUpdate: {type: object, properties: {metadata: {}, cancel_at_period_end: {type: boolean}}, minProperties: 1, additionalProperties: false}
    ChangePreviewRequest: {type: object, properties: {price: {type: string}, effective_at: {enum: [now, current_period_end]}, proration_behavior: {enum: [create_prorations, always_invoice, none]}, billing_cycle_anchor: {enum: [unchanged, now]}, payment_behavior: {enum: [pending_if_incomplete, allow_incomplete]}}, required: [price], additionalProperties: false}
    ChangePreview: {type: object, properties: {id: {type: string}, object: {const: subscription_change_preview}, subscription: {type: string}, target_price: {type: string}, quote_id: {type: string}, source_revision: {type: integer}, policy_revision: {type: integer}, effective_at: {type: string, format: date-time}, proration_date: {type: string, format: date-time}, quote_expires_at: {type: string, format: date-time}, currency: {type: string}, amounts: {type: object, properties: {paid_service_credit_minor: {type: integer}, immediate_delta_minor: {type: integer}, immediate_due_minor: {type: integer}, deferred_delta_minor: {type: integer}, next_period_amount_minor: {type: integer}}, required: [paid_service_credit_minor, immediate_delta_minor, immediate_due_minor, deferred_delta_minor, next_period_amount_minor]}, options: {type: object, properties: {effective_at: {enum: [now, current_period_end]}, proration_behavior: {enum: [create_prorations, always_invoice, none]}, billing_cycle_anchor: {enum: [unchanged, now]}, payment_behavior: {enum: [pending_if_incomplete, allow_incomplete]}}, required: [effective_at, proration_behavior, billing_cycle_anchor, payment_behavior]}}, required: [id, object, subscription, target_price, quote_id, source_revision, policy_revision, effective_at, proration_date, quote_expires_at, currency, amounts, options]}
    SubscriptionChange: {type: object, properties: {id: {type: string}, object: {const: subscription_change}, subscription: {type: string}, target_price: {type: string}, status: {enum: [scheduled, pending_payment, applied]}, effective_at: {type: string, format: date-time}, invoice: {type: [string, 'null']}, source_revision: {type: integer}, created_at: {type: string, format: date-time}}, required: [id, object, subscription, target_price, status, effective_at, invoice, source_revision, created_at]}
    PauseCollection: {type: object, properties: {behavior: {type: [string, 'null'], enum: [keep_as_draft, void, mark_uncollectible, null]}, resumes_at: {type: [string, 'null'], format: date-time}}, additionalProperties: false}
    BillingPolicy: {type: object, properties: {policy_id: {type: [string, 'null']}, revision: {type: integer}, defaults: {type: object}, allowed_overrides: {type: object}}, required: [policy_id, revision, defaults, allowed_overrides]}
    WebhookEndpointInput: {type: object, properties: {url: {type: string, format: uri}, event_types: {type: array, items: {type: string}, minItems: 1}}, required: [url, event_types], additionalProperties: false}
    WebhookEndpointUpdate: {type: object, properties: {url: {type: string, format: uri}, event_types: {type: array, items: {type: string}}, active: {type: boolean}, rotate_secret: {type: boolean}}, minProperties: 1, additionalProperties: false}
  requestBodies:
    Product: {required: true, content: {application/json: {schema: {type: object, properties: {name: {type: string, minLength: 1, maxLength: 200}, description: {type: [string, 'null'], maxLength: 2000}, metadata: {type: [object, 'null']}}, required: [name], additionalProperties: false}}}}
    Price: {required: true, content: {application/json: {schema: {type: object, properties: {product_id: {type: string}, unit_amount: {type: integer, minimum: 1}, currency: {type: string, const: ZAR}, recurring: {type: object, properties: {interval: {enum: [DAY, WEEK, MONTH, YEAR]}, interval_count: {type: integer, minimum: 1}}, required: [interval], additionalProperties: false}, metadata: {type: [object, 'null']}}, required: [product_id, unit_amount, currency, recurring], additionalProperties: false}}}}
    Customer: {required: true, content: {application/json: {schema: {type: object, properties: {external_id: {type: [string, 'null'], maxLength: 200}, email: {type: [string, 'null'], format: email}, name: {type: [string, 'null'], maxLength: 200}, metadata: {type: [object, 'null']}}, additionalProperties: false}}}}
    CustomerPatch: {required: true, content: {application/json: {schema: {type: object, properties: {external_id: {type: [string, 'null'], maxLength: 200}, email: {type: [string, 'null'], format: email}, name: {type: [string, 'null'], maxLength: 200}, metadata: {type: [object, 'null']}}, minProperties: 1, additionalProperties: false}}}}
    Checkout: {required: true, content: {application/json: {schema: {type: object, properties: {mode: {const: subscription}, customer: {type: string}, price: {type: string}, purchase_reference: {type: string, minLength: 1, maxLength: 200}, success_url: {type: string, format: uri}, cancel_url: {type: string, format: uri}}, required: [mode, customer, price, purchase_reference], additionalProperties: false}}}}
    WebhookEndpoint: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/WebhookEndpointInput'}}}}
    WebhookEndpointUpdate: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/WebhookEndpointUpdate'}}}}
  responses:
    Error: {description: Standard billing API error, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
    Product: {description: Product resource, content: {application/json: {schema: {$ref: '#/components/schemas/Product'}}}}
    Price: {description: Price resource, content: {application/json: {schema: {$ref: '#/components/schemas/Price'}}}}
    Customer: {description: Customer resource, content: {application/json: {schema: {$ref: '#/components/schemas/Customer'}}}}
    CheckoutSession: {description: Hosted checkout session, content: {application/json: {schema: {$ref: '#/components/schemas/CheckoutSession'}}}}
    Subscription: {description: Subscription resource, content: {application/json: {schema: {$ref: '#/components/schemas/Subscription'}}}}
    Invoice: {description: Invoice resource, content: {application/json: {schema: {$ref: '#/components/schemas/Invoice'}}}}
    Event: {description: Billing event resource, content: {application/json: {schema: {$ref: '#/components/schemas/BillingEvent'}}}}
    WebhookEndpoint: {description: Endpoint metadata; signing secret omitted, content: {application/json: {schema: {$ref: '#/components/schemas/WebhookEndpoint'}}}}
    WebhookEndpointWithSecret: {description: Endpoint created or rotated; signing_secret appears only once, content: {application/json: {schema: {allOf: [{$ref: '#/components/schemas/WebhookEndpoint'}, {type: object, properties: {signing_secret: {type: string}}, required: [signing_secret]}]}}}}
    ProductPage: {description: Product cursor page, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Product'}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}}}}
    PricePage: {description: Price cursor page, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Price'}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}}}}
    CustomerPage: {description: Customer cursor page, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Customer'}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}}}}
    SubscriptionPage: {description: Subscription cursor page, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Subscription'}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}}}}
    InvoicePage: {description: Invoice cursor page, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Invoice'}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}}}}
    EventPage: {description: Event cursor page, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/BillingEvent'}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}}}}
    WebhookEndpointPage: {description: Endpoint cursor page, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/WebhookEndpoint'}}, next_cursor: {type: [string, 'null']}}, required: [data, next_cursor]}}}}
