> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bouclay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a subscription

> A usage charge on a meter another of the customer's live subscriptions bills (active, past due, trialing, paused, waiting for its first payment, or with a change queued to start it) is refused with 422 `meter_billed_elsewhere`, whose `subscriptionId` and `meterId` name the other subscription and the meter: every event would otherwise be billed on both. A subscription that ends at or before the new charge starts doesn't count. Plan changes and item changes refuse the same way.



## OpenAPI

````yaml /api/openapi.yaml post /subscriptions
openapi: 3.1.0
info:
  title: Bouclay API
  version: 1.0.0
  summary: Integrator API for Bouclay subscriptions, invoices, payments, and webhooks.
  description: >
    Bouclay is managed recurring-billing infrastructure for African payment

    gateways. Integrators connect their own gateway account (Bachs, with Nomba,

    Paystack and Flutterwave in early access) and use this API to manage
    customers, catalog, subscriptions,

    entitlements, usage metering, invoices, payment-method checkout sessions,
    payments, and

    outbound event history.


    Bouclay never holds funds — every charge, refund, and payout happens on your

    own gateway account, with your own keys. Which gateway settles a given
    charge

    is Bouclay's concern, not yours: a stored card always charges through the

    gateway that issued its token.


    ## Early access


    Some of this API is documented ahead of its release, so you can build

    against it now. Those operations carry an **Early access** badge, and so

    do the request fields that depend on them (graduated, volume and package

    pricing, usage-based prices, usage thresholds). Until a feature is turned

    on for your account, its endpoints answer `404 not_found`, its fields are

    refused with a `422`, and a restricted key cannot be given its scopes.

    Ask us to turn it on. Nothing you already rely on changes when it is.
servers:
  - url: https://bouclay.com/api/v1
    description: Production
security:
  - SecretKeyAuth: []
tags:
  - name: Customers
  - name: Customer addresses
  - name: Payment methods
  - name: Entitlements
    description: >-
      Early access: answers `404 not_found` until it is turned on for your
      account.
  - name: Credits
    description: >-
      What a customer has left to spend. Money owed back to a customer is their
      balance, one per currency; their next invoice spends it after tax, like a
      payment, and collects the rest. Promotional credit you give is held in
      lots (credit grants), each with its own expiry; the next invoice spends it
      before tax, so it lowers the VAT like a discount.
  - name: Catalog
  - name: Payment links
    description: >-
      A shareable checkout page for one price, the same link a price's "Payment
      link" makes in the dashboard. Buyers enter their email and pay; a
      subscription price starts the subscription once paid.
  - name: Meters
    description: >-
      Early access: answers `404 not_found` until it is turned on for your
      account.
  - name: Usage
    description: >-
      Early access: answers `404 not_found` until it is turned on for your
      account.
  - name: Discounts
    description: >-
      Early access: answers `404 not_found` until it is turned on for your
      account.
  - name: Subscriptions
  - name: Invoices
  - name: Metrics
    description: >-
      Early access: answers `404 not_found` until it is turned on for your
      account. Revenue metrics measured from each subscription's monthly
      recurring amount (MRR). `active` and `past_due` subscriptions count;
      trials, pauses and metered usage do not. Amounts are major units, rates
      are percentages (105.25 means 105.25%), and currencies are never summed.
  - name: Credit notes
    description: >-
      A credit note corrects an issued invoice without changing it: "of this
      invoice, this much is no longer owed". Each one has its own number in an
      unbroken sequence (`{prefix}-CN-0001`) and never changes once issued. On
      an unpaid invoice the credit comes off `amountDue`; on a paid one it goes
      back to the customer, refunded to the card, added to the customer balance
      or settled outside Bouclay. What the customer balance paid of an invoice
      always goes back to the balance first. An invoice always satisfies
      `amountDue = total - amountPaid - amountCredited - balanceApplied`. Every
      refund also issues a credit note.
  - name: Payments
  - name: Refunds
    description: >-
      A refund goes back through the gateway that took the payment. Refund part
      of a payment and it stays `succeeded`; refund all of it and it becomes
      `refunded`.
  - name: Chargebacks
    description: >-
      A chargeback is a payment the customer's bank took back. Record one when
      your payment provider reports it, with the provider's dispute reference,
      then record how the dispute ended. While it is open, Bouclay doesn't
      charge or chase the subscription. A chargeback on a credit top-up takes
      back the unspent credit and its bonus; winning the dispute puts back
      exactly that. Uses the `refunds` scope.
  - name: Events
  - name: Checkout sessions
  - name: Portal sessions
  - name: Webhook endpoints
paths:
  /subscriptions:
    post:
      tags:
        - Subscriptions
      summary: Create a subscription
      description: >-
        A usage charge on a meter another of the customer's live subscriptions
        bills (active, past due, trialing, paused, waiting for its first
        payment, or with a change queued to start it) is refused with 422
        `meter_billed_elsewhere`, whose `subscriptionId` and `meterId` name the
        other subscription and the meter: every event would otherwise be billed
        on both. A subscription that ends at or before the new charge starts
        doesn't count. Plan changes and item changes refuse the same way.
      operationId: createSubscription
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
      responses:
        '201':
          $ref: '#/components/responses/Subscription'
        '400':
          $ref: '#/components/responses/Error'
        '422':
          $ref: '#/components/responses/Error'
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 255
      description: >-
        Required for POST and PATCH requests. Replays identical method + path +
        body requests and rejects mismatched re-use.
  schemas:
    CreateSubscriptionRequest:
      type: object
      required:
        - customer
        - collectionMode
        - items
      properties:
        customer:
          type: string
        collectionMode:
          type: string
          enum:
            - automatic
            - manual
          description: >-
            `automatic` charges a card. A subscription with something due today
            opens `incomplete` and starts on its first payment. `manual` sends
            invoices to pay by transfer or on the hosted invoice page: it opens
            `active` at once, on its payment terms (`daysUntilDue`), and an
            invoice still unpaid a day after it's due turns it `past_due` and is
            chased by reminder emails, then suspended as your dunning settings
            say.
        paymentMethod:
          type: string
        trialEndBehavior:
          type: string
          enum:
            - cancel
            - pause
            - create_invoice
        daysUntilDue:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 365
          description: >-
            Payment terms for this subscription's manual invoices: each is due
            this many days after it's issued (0 is due on receipt). Left out,
            the business's payment terms apply (7 days unless changed in Billing
            settings, Invoicing). Manual dunning counts from the due date.
            Ignored for automatic collection.
        reference:
          type: string
          maxLength: 100
          description: >-
            The buyer's purchase order number or other reference, printed as
            "Your reference" on every invoice the subscription issues (PDF,
            hosted page, invoice and receipt emails).
        requirePaymentMethod:
          type: boolean
          description: |
            Overrides the business setting for this subscription. When nothing
            is due at signup (every paid price is billed at the end of the
            period) and no `paymentMethod` is given, `true` opens the
            subscription `incomplete` behind a small card-verification invoice
            that is refunded once it settles; `false` starts it `active` with
            no card. Ignored for `manual` collection and free subscriptions.

            On a free trial the business setting doesn't apply: a trial needs
            a card when its price has `trialRequiresPaymentInfo`, or when this
            is `true`. It then opens `incomplete` behind the card check and
            becomes `trialing` once the card is saved; abandoned there, it
            expires like any unpaid start and doesn't use up the customer's
            trial.
        invoicingThreshold:
          $ref: '#/components/schemas/MajorAmount'
          description: |
            **Early access:** refused until metered billing is turned on for
            your account.

            Invoice usage early once what the subscription has used this
            period, and not yet been billed for, reaches this amount. Fixed
            fees never count toward it. The period's own invoice then bills
            only the rest.
        minimumCommitment:
          $ref: '#/components/schemas/MajorAmount'
          description: |
            **Early access:** refused until metered billing is turned on for
            your account.

            The least the subscription bills each billing period, in major
            units of its currency. Fees, usage and prorations count toward it
            after discounts and before tax; credit the customer spends still
            counts. When a period bills less, the invoice that closes it adds
            a `minimum` line for the difference (with the next period's fees
            when the subscription bills up front, or on the final invoice when
            it is cancelled). A trial owes none; a period cut short by a
            cancellation, or paused for part of it, owes its share.
        spendingCap:
          $ref: '#/components/schemas/SpendingCapInput'
        discountCode:
          type: string
          maxLength: 255
          description: |
            A discount code as the customer typed it, matched without regard
            to case. It is redeemed onto the subscription and takes money off
            its invoices from the first one (after a trial, the conversion
            invoice). A code no discount has, or one that has expired, is
            switched off, is used up or doesn't cover these prices, is refused
            with a `422` on `discountCode` saying why, and nothing is created.
            Not with `discount`.
        discount:
          type: string
          description: >-
            A discount's id (`disc_…`), in place of `discountCode`. Refused the
            same way.
        items:
          type: array
          minItems: 1
          description: |
            The base plan first, then add-on plans. Each plan is taken whole:
            a plan made of several charges goes on as one item per active
            charge, each at its price in one way to pay. Name it with a
            `planId` (and `billingInterval` when it offers several), or with
            one or more of its prices: the other charges are filled in with
            the only price each offers in that way to pay. A plan with one
            charge is the price given, as always. A meter is billed by one
            charge per subscription.
          items:
            type: object
            properties:
              priceId:
                type: string
              price:
                type: string
                description: Backward-compatible alias accepted by the API.
              quantity:
                type: integer
                minimum: 1
                maximum: 1000
                default: 1
                description: The quantity of the named price's charge.
              planId:
                type: string
                description: A whole plan, in place of a `priceId`.
              billingInterval:
                type: string
                enum:
                  - day
                  - week
                  - month
                  - year
                description: With `planId`, the way to pay, in the customer's currency.
              billingFrequency:
                type: integer
                minimum: 1
                default: 1
              quantities:
                type: object
                additionalProperties:
                  type: integer
                  minimum: 1
                  maximum: 1000
                description: >-
                  With `planId`, quantities by charge id or code, such as
                  `{"seats": 10}`. A charge left out starts at its price's
                  fewest, else 1.
    MajorAmount:
      type:
        - number
        - 'null'
      minimum: 0
      description: Major currency units. For example, 15000 means NGN 15,000.
    SpendingCapInput:
      type: object
      required:
        - amount
      description: |
        **Early access:** refused until metered billing is turned on for
        your account.

        The most the subscription bills each billing period. Spend is what
        the period bills, after discounts and before tax: its usage
        (`covers: usage`, the default) or everything it bills
        (`covers: all`). A cap that stops billing (`behavior: cap`, the
        default) takes what goes past it off the invoice that bills it, with
        a negative `maximum` line; fees billed up front are always billed in
        full. One that only warns (`behavior: warn`) bills in full. At each
        point in `warnAt` (percent of the cap) your app gets
        `spend_alert.created`, once per period and cap.
      properties:
        amount:
          $ref: '#/components/schemas/MajorAmount'
        covers:
          type: string
          enum:
            - usage
            - all
        behavior:
          type: string
          enum:
            - cap
            - warn
        warnAt:
          type: array
          maxItems: 5
          items:
            type: integer
            minimum: 1
            maximum: 100
          example:
            - 80
            - 100
    ResourceEnvelope:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          type: object
        meta:
          $ref: '#/components/schemas/Meta'
    Subscription:
      type: object
      properties:
        id:
          type: string
        planId:
          type:
            - string
            - 'null'
          description: >-
            The base plan, the one its live plan items are on. Kept in step once
            each plan change, retirement or price swap commits; a canceled
            subscription keeps the plan it was on.
        status:
          type: string
        pauseReason:
          description: >-
            Why a paused subscription is paused. `payment_overdue` means dunning
            suspended it: access is off, the invoice stays open, and paying it
            resumes the subscription. Voiding it or marking it uncollectible
            turns the suspension into `requested`. `plan_retired` means its plan
            was discontinued with nothing in its place: move it to another plan,
            then resume it.
          type:
            - string
            - 'null'
          enum:
            - requested
            - payment_overdue
            - plan_retired
            - null
        pauseResumesAt:
          description: When a paused subscription resumes on its own, else null.
          type:
            - string
            - 'null'
          format: date-time
        currency:
          type: string
        collectionMode:
          type: string
          enum:
            - automatic
            - manual
        daysUntilDue:
          type:
            - integer
            - 'null'
          description: >-
            The subscription's own payment terms in days; null means the
            business's payment terms apply.
        reference:
          type:
            - string
            - 'null'
          description: The buyer's purchase order number, printed on its invoices.
        invoicingThreshold:
          $ref: '#/components/schemas/MajorAmount'
        minimumCommitment:
          $ref: '#/components/schemas/MajorAmount'
          description: >-
            The minimum commitment in force: the least the subscription bills
            each period, in major units. Null when it has none. A change is sent
            as `subscription.updated`.
        spendingCap:
          description: >-
            The spending cap in force, or null. A change is sent as
            `subscription.updated`.
          oneOf:
            - $ref: '#/components/schemas/SpendingCap'
            - type: 'null'
        trialEndsAt:
          type:
            - string
            - 'null'
          format: date-time
        currentPeriodStart:
          type:
            - string
            - 'null'
          format: date-time
        currentPeriodEnd:
          type:
            - string
            - 'null'
          format: date-time
        canceledAt:
          type:
            - string
            - 'null'
          format: date-time
        endsAt:
          type:
            - string
            - 'null'
          format: date-time
        cancellationDetails:
          description: >-
            Why it was cancelled, while it is cancelling or canceled. Null
            otherwise, including after the cancel is undone.
          type:
            - object
            - 'null'
          properties:
            reason:
              type:
                - string
                - 'null'
            comment:
              type:
                - string
                - 'null'
            source:
              type: string
              enum:
                - portal
                - dashboard
                - api
        customer:
          $ref: '#/components/schemas/NestedCustomer'
        discount:
          description: >-
            The discount still taking money off its invoices, or null (none, or
            used up). `remainingIntervals` is how many more invoices it applies
            to; null means for as long as the subscription runs.
          type:
            - object
            - 'null'
          properties:
            id:
              type: string
            code:
              type:
                - string
                - 'null'
            remainingIntervals:
              type:
                - integer
                - 'null'
        createdAt:
          type:
            - string
            - 'null'
          format: date-time
        trialEndBehavior:
          type:
            - string
            - 'null'
        paymentMethod:
          type:
            - object
            - 'null'
          properties:
            id:
              type: string
        items:
          type: array
          items:
            $ref: '#/components/schemas/SubscriptionItem'
        scheduledChanges:
          type: array
          description: >-
            Changes booked for a later date, usually the next renewal, each with
            what it changes. Fields that don't apply to an action are left out.
          items:
            type: object
            properties:
              action:
                type: string
                enum:
                  - cancel
                  - pause
                  - resume
                  - update
                  - change_plan
                  - add_charge
                  - end_charge
              effectiveAt:
                type:
                  - string
                  - 'null'
                format: date-time
              itemId:
                description: '`update`: the item that changes.'
                type: string
                example: si_123
              quantity:
                description: >-
                  `update`: the item's new quantity, or null when it keeps its
                  own.
                type:
                  - integer
                  - 'null'
                example: 2
              priceId:
                description: '`update`: the item''s new price, or null when it keeps its own.'
                type:
                  - string
                  - 'null'
              remove:
                description: '`update`: true when the item is removed.'
                type: boolean
              planId:
                description: '`change_plan`: the plan the subscription moves onto.'
                type: string
              chargeId:
                description: >-
                  `add_charge` and `end_charge`: the plan's charge that starts
                  or ends.
                type: string
              resumesAt:
                description: '`pause`: when the subscription comes back on its own, or null.'
                type:
                  - string
                  - 'null'
                format: date-time
        sharedMeters:
          type: array
          description: >-
            Meters another of the customer's live subscriptions bills too. Meter
            events carry no subscription, so each such event is billed on both.
            A new overlap is refused (`meter_billed_elsewhere`); this lists one
            that slipped through, such as a payment link paid while another
            subscription started. Empty when there is none.
          items:
            type: object
            properties:
              meterId:
                type: string
              meterName:
                type: string
              subscriptionIds:
                type: array
                items:
                  type: string
    ErrorResponse:
      type: object
      required:
        - error
        - meta
      properties:
        error:
          type: object
          required:
            - type
            - code
            - detail
          properties:
            type:
              type: string
              enum:
                - request_error
                - api_error
              description: >-
                `api_error` with code `internal_error` (HTTP 500) means the
                fault is ours: retry with the same Idempotency-Key.
              example: request_error
            code:
              type: string
              enum:
                - authentication_failed
                - permission_denied
                - invalid_field
                - invalid_cursor
                - idempotency_conflict
                - meter_not_found
                - meter_archived
                - customer_not_found
                - timestamp_in_future
                - usage_period_closed
                - subscription_paused
                - missing_property
                - invalid_value
                - conflict
                - subscription_suspended
                - no_upcoming_invoice
                - not_found
                - request_error
                - internal_error
            detail:
              type: string
            errors:
              type: array
              items:
                type: object
                required:
                  - field
                  - message
                properties:
                  field:
                    type: string
                  message:
                    type: string
        meta:
          $ref: '#/components/schemas/Meta'
    Meta:
      type: object
      required:
        - requestId
      properties:
        requestId:
          type: string
          example: req_550e8400-e29b-41d4-a716-446655440000
    SpendingCap:
      type: object
      properties:
        amount:
          $ref: '#/components/schemas/MajorAmount'
        covers:
          type: string
          enum:
            - usage
            - all
        behavior:
          type: string
          enum:
            - cap
            - warn
        warnAt:
          type: array
          items:
            type: integer
        since:
          type: string
          format: date-time
    NestedCustomer:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type:
            - string
            - 'null'
        taxId:
          description: >-
            On an invoice, the customer's TIN as it was when the invoice was
            issued.
          type:
            - string
            - 'null'
    SubscriptionItem:
      type: object
      properties:
        id:
          type: string
        productId:
          type: string
        planId:
          type: string
        chargeId:
          type: string
          description: Which of the plan's charges the item bills.
        priceId:
          type: string
        quantity:
          type: integer
        status:
          type: string
  responses:
    Subscription:
      description: Subscription response.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ResourceEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
    Error:
      description: Structured API error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    SecretKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: '{sk|rk}_{mode}_{random}'
      description: >
        Use a Bouclay secret key (`sk_test_...`, `sk_live_...`) or restricted
        key

        (`rk_test_...`, `rk_live_...`).


        A secret key can call every endpoint. A restricted key can only call the

        endpoints its scopes cover. A scope is `{resource}:{access}`, for
        example

        `customers:read` or `refunds:write`, and write includes read. `GET`

        requests need read access and everything else needs write, except

        `POST /subscriptions/{id}/items/{item}/preview`, which only needs

        `subscriptions:read`. A request outside the key's scopes gets `403

        permission_denied` naming the missing scope, and its `Idempotency-Key`

        is not used up.


        | Resource | Endpoints | Access |

        | --- | --- | --- |

        | `customers` | customers, their addresses and payment methods | read,
        write |

        | `entitlements` | `/customers/{id}/entitlements` | read |

        | `credits` | `/customers/{id}/credit-balance`, credit purchases, credit
        grants, auto-reloads, credit holds (committing one records its usage) |
        read, write |

        | `catalog` | products, plans, prices | read, write |

        | `meters` | meters | read, write |

        | `usage` | meter events, `/customers/{id}/usage` | read, write |

        | `discounts` | discounts | read |

        | `subscriptions` | subscriptions | read, write |

        | `invoices` | invoices | read, write |

        | `credit_notes` | credit notes, `POST /invoices/{id}/credit-notes` (a
        refund to the card also needs `refunds:write`) | read, write |

        | `payments` | payments | read |

        | `refunds` | refunds, `POST /payments/{id}/refunds`, chargebacks and
        their outcome | read, write |

        | `checkout_sessions` | checkout sessions | read, write |

        | `portal_sessions` | portal sessions | write |

        | `events` | events | read |

        | `webhook_endpoints` | webhook endpoints | read, write |

        | `metrics` | metrics | read |


        Publishable keys (`pk_...`) are rejected by server-side API endpoints. A

        key's mode scopes what it can see and charge: a test key can never reach

        a live card.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.