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

> ## Agent Instructions
> Money is always a decimal string at the currency's precision (for example "29.00"), paired with an ISO 4217 currency field. Never use minor units.
> Build against the sandbox first: base URL https://sandbox-api.bachs.io with sk_sandbox_ keys. Production is https://api.bachs.io with sk_live_ keys; going live is a key swap.
> Treat webhooks (for example collection.succeeded) as the source of truth for fulfilment, never client-side events or redirects.
> Subscriptions are created by completing a checkout for a recurring product. There is no direct create-subscription endpoint.
> IDs carry resource prefixes (cust_, prod_, sub_, chk_, inv_, ref_) and timestamps are ISO 8601 UTC.

# Create a charge

> **In beta.** This endpoint might change, including field names and the shape of the response. Pin your integration to what you test.

Charge a customer's saved card off-session, meaning with nobody on a payment page. Use this for money a customer agreed to once and you collect later, such as a usage invoice or a top-up. An off-session charge cannot ask the customer to authenticate, so a card whose issuer demands it is refused.

The card must have been saved on an earlier checkout. See [Charge a saved card](/guides/payments/charge-a-saved-card).

This always answers with a payment, never an error, when the card is refused: a refusal is an outcome you read from `status`. The payment is usually `processing`, and the result reaches you as a `collection.succeeded` or `collection.failed` webhook. A card refused while the request is still open comes back already `failed`. A `201` is not payment received.

Send an `Idempotency-Key` header. Without one, a retry after a timeout charges the customer twice.



## OpenAPI

````yaml /docs/openapi/openapi.json post /v1/charges
openapi: 3.0.3
info:
  title: Bachs API
  version: 1.0.0
  description: >-
    The Bachs API lets you accept payments, manage subscriptions, and move money
    across African markets.


    - **Authentication:** pass your secret key as `Authorization: Bearer
    sk_...`. See [Authentication](/authentication) for keys, scopes, and sandbox
    vs production.

    - **Errors:** every error returns a flat `{ "detail", "error_code",
    "doc_url" }`. See [Errors](/errors).

    - **Pagination:** list endpoints return `{ items, pagination }`. See
    [Pagination](/guides/pagination).

    - **Idempotency:** retry `POST` safely with the `Idempotency-Key` header.
    See [Idempotency](/guides/idempotency).

    - **Base URLs and rate limits:** see the
    [Introduction](/api-reference/overview).
  contact:
    name: Bachs Support
    url: https://bachs.io
servers:
  - url: https://api.bachs.io
    description: Production
  - url: https://sandbox-api.bachs.io
    description: Sandbox
security:
  - ApiKeyAuth: []
tags:
  - name: Payments
    description: Accept payments from customers
  - name: Refunds
    description: Create and manage customer refunds
  - name: Disputes
    description: Respond to chargebacks and manage dispute evidence
  - name: Balances
    description: View balances and account information
  - name: Conversions
    description: Quote, execute, and query currency conversions
  - name: Payouts
    description: Withdraw funds to external accounts
  - name: Webhooks
    description: Receive real-time payment notifications
  - name: Authentication
    description: API key management and authentication
  - name: Customers
    description: Create and manage customers
  - name: Products
    description: Define and manage your billing catalog
  - name: Product Groups
    description: Bundle products for multi-plan checkout
  - name: Media
    description: Upload files and get an upload_id to attach to products
  - name: Checkouts
    description: Create and manage API-driven checkouts without products
  - name: Subscriptions
    description: >-
      Recurring billing. Subscriptions are created through checkout; manage them
      here.
  - name: Customer sessions
    description: >-
      Open a hosted portal session so a customer can manage their own
      subscriptions, invoices and cards.
  - name: Accounts
    description: >-
      Create the accounts your platform onboards, request their capabilities,
      walk them through their requirements, and read what they still owe.
  - name: Transfers
    description: Move funds between your platform and the accounts you own.
  - name: Platform Fees
    description: Read the platform's cut of an account's sale.
paths:
  /v1/charges:
    post:
      tags:
        - Payments
      summary: Create a charge
      description: >-
        **In beta.** This endpoint might change, including field names and the
        shape of the response. Pin your integration to what you test.


        Charge a customer's saved card off-session, meaning with nobody on a
        payment page. Use this for money a customer agreed to once and you
        collect later, such as a usage invoice or a top-up. An off-session
        charge cannot ask the customer to authenticate, so a card whose issuer
        demands it is refused.


        The card must have been saved on an earlier checkout. See [Charge a
        saved card](/guides/payments/charge-a-saved-card).


        This always answers with a payment, never an error, when the card is
        refused: a refusal is an outcome you read from `status`. The payment is
        usually `processing`, and the result reaches you as a
        `collection.succeeded` or `collection.failed` webhook. A card refused
        while the request is still open comes back already `failed`. A `201` is
        not payment received.


        Send an `Idempotency-Key` header. Without one, a retry after a timeout
        charges the customer twice.
      operationId: createCharge
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateChargeRequest'
            example:
              customer: cust_1a2b3c4d5e6f
              amount: '29.00'
              currency: USD
              description: April usage
              reference: INV-2026-04-881
      responses:
        '201':
          description: Success - Charge created and submitted to the card
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentResponse'
              example:
                payment_id: ch_389305e973a841cc
                status: processing
                amount: '30.85'
                amount_paid: '0.00'
                amount_remaining: '30.85'
                currency: USD
                fees:
                  amount: '1.85'
                  currency: USD
                payment_method: CARD
                checkout_id: null
                narration: April usage
                reference: INV-2026-04-881
                billing_reason: purchase
                customer:
                  name: Jane Doe
                  email: jane@example.com
                created_at: '2026-04-27T12:05:00.000Z'
                updated_at: '2026-04-27T12:05:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - ApiKeyAuth: []
components:
  schemas:
    CreateChargeRequest:
      type: object
      required:
        - customer
        - amount
        - currency
      properties:
        customer:
          type: string
          description: >-
            The customer to charge, by their `cust_` id. They must already have
            a saved card, or the request is refused with
            `NO_SAVED_PAYMENT_METHOD`.
        payment_method:
          type: string
          nullable: true
          description: >-
            The saved card to charge, by its `pm_` id. Omit to charge the
            customer's default saved card, which is the first card they saved. A
            card belonging to a different customer is refused with
            `SAVED_PAYMENT_METHOD_NOT_FOUND`.
        amount:
          type: string
          description: >-
            The amount to collect, as a decimal string (for example `"29.00"`).
            Must be greater than zero. This is the amount before the processing
            fee: when your account passes the fee to the customer, the card is
            charged more than this and the response `amount` shows the total.
        currency:
          type: string
          description: >-
            The currency of `amount`, in [ISO
            4217](https://en.wikipedia.org/wiki/ISO_4217) format (for example
            `USD`). Your account must be able to collect card payments in this
            currency, or the request is refused with
            `PAYMENT_METHOD_NOT_ENABLED`.
        description:
          type: string
          nullable: true
          maxLength: 255
          description: >-
            What the charge is for. Returned as `narration` on the payment, and
            shown on your dashboard.
        reference:
          type: string
          nullable: true
          maxLength: 128
          description: >-
            Your own identifier for this charge, returned unchanged on the
            payment so you can match it to your records.
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: >-
            Key-value pairs you attach to the charge and get back on the payment
            and its webhooks.
    PaymentResponse:
      type: object
      description: Detailed payment response for API integrations.
      properties:
        reference:
          type: string
          nullable: true
          description: Checkout reference when available.
          example: order_9876
        payment_id:
          type: string
          description: Unique identifier for the payment.
          example: pay_1a2b3c4d5e
        billing_reason:
          type: string
          enum:
            - purchase
            - subscription_create
            - subscription_cycle
            - subscription_update
          description: >-
            Why this payment exists. `purchase`: a one-time purchase.
            `subscription_create`: the first cycle of a new subscription.
            `subscription_cycle`: a subscription renewal. `subscription_update`:
            an off-cycle charge from a mid-cycle plan change (proration).
          example: purchase
        checkout_id:
          type: string
          nullable: true
          description: Checkout identifier, when linked.
          example: chk_8T9u0V1w2X3y4Z5a
        status:
          type: string
          description: >-
            Current status of the payment. `created`: the charge exists and no
            attempt has succeeded yet. `processing`: an attempt is in flight and
            is being verified. `succeeded`: the payment is confirmed and settled
            in full. `accepted`: an underpayment or overpayment was accepted as
            final settlement. `failed`: the payment failed and no funds were
            captured. `expired`: the payment window elapsed before any payment
            arrived. `cancelled`: cancelled before completion. `refunded`: the
            full amount was returned to the customer. `partially_refunded`: part
            of the amount was returned to the customer. `auto_refunded`: we
            automatically returned the full amount to the customer. `underpaid`:
            the customer paid less than the amount owed. `overpaid`: the
            customer paid more than the amount owed.
          enum:
            - created
            - processing
            - succeeded
            - accepted
            - failed
            - expired
            - cancelled
            - refunded
            - partially_refunded
            - auto_refunded
            - underpaid
            - overpaid
          x-enum-descriptions:
            created: Charge created and awaiting payment.
            processing: Payment received and being verified.
            succeeded: Payment confirmed and fully settled.
            accepted: Payment accepted by the processor, pending final confirmation.
            failed: Payment failed; no funds captured.
            expired: Charge expired before payment was received.
            cancelled: Charge cancelled before completion.
            refunded: Full refund issued.
            partially_refunded: Partial refund issued.
            auto_refunded: Full refund issued automatically.
            underpaid: Payment received was less than the requested amount.
            overpaid: Payment received exceeded the requested amount.
          example: succeeded
        is_refundable:
          type: boolean
          nullable: true
          description: Whether this payment can currently be refunded.
          example: true
        amount:
          type: string
          description: Requested amount in `currency`.
          example: '10.00'
        amount_paid:
          type: string
          nullable: true
          description: Amount received so far.
          example: '10.00'
        amount_remaining:
          type: string
          nullable: true
          description: Remaining amount still expected.
          example: '0.00'
        currency:
          type: string
          description: Payment currency code.
          example: USD
        fee_usd:
          type: string
          description: >-
            Processing fee for this payment, converted to USD and expressed as a
            decimal string. `null` until the payment settles.
          example: '0.59'
          nullable: true
        fees:
          allOf:
            - $ref: '#/components/schemas/FeeInfo'
          nullable: true
          description: >-
            The processing fee on this payment, in the currency it was charged
            in. Prefer this over `fee_usd` when the payment was not collected in
            USD: `fee_usd` is a conversion of the same fee, this is the amount
            actually taken. `null` on a payment that carries no processing fee.
            See [Fees](/for-you/fees).
        merchant_bears_cost:
          type: boolean
          nullable: true
          description: Whether merchant bears processing cost.
          example: true
        platform_fee:
          type: string
          nullable: true
          description: >-
            What the platform took out of this sale, beside the gross it was
            taken from, in the base currency of the sale. `null` when the charge
            carried no platform fee, and on a charge that split the sale with
            `transfer_data.amount` instead. See [Platform
            fees](/connect/platform-fees).
          example: null
        destination_amount:
          type: string
          nullable: true
          description: >-
            The seller's contracted share of this sale, in the base currency of
            the sale. Null on a charge that carries no split, and on one that
            split the sale with `platform_fee` instead.
          example: null
        fee_paid_by:
          type: string
          nullable: true
          enum:
            - merchant
            - platform
          description: >-
            Who Bachs's processing fee actually came from on this charge, read
            back from the ledger posting rather than a flag decided in advance.
            `merchant`: the fee came out of the charge. `platform`: the
            platform's own balance covered it. On a destination charge this
            never reads `platform`; the fee always comes from the charge there.
            See [Processing fees](/connect/processing-fees).
          example: merchant
        payment_method:
          type: string
          nullable: true
          description: >-
            Payment method used for this payment. For every method except card,
            this is the exact corridor collected, such as `NGN_BANK_TRANSFER`,
            `MOMO_GHS`, or `CRYPTO`. Card charges report `CARD` rather than
            `USD_CARD` or `NGN_CARD`; read the currency to tell which card
            corridor collected it.
          example: NGN_BANK_TRANSFER
        channel:
          type: string
          nullable: true
          description: Origin channel (for example `api`).
          example: checkout
        narration:
          type: string
          nullable: true
          description: payment description/narration.
          example: Pro plan
        meta:
          type: object
          nullable: true
          additionalProperties: true
          description: Public metadata stored for this payment.
          example:
            order_id: ORD-9876
        message:
          type: string
          nullable: true
          description: Human-readable payment message derived from status.
          example: Successful
        customer:
          type: object
          nullable: true
          description: Customer information when available.
          properties:
            name:
              type: string
              nullable: true
              description: >-
                Full name of the customer associated with this payment, when
                captured.
            email:
              type: string
              nullable: true
              description: >-
                Customer email address associated with this payment, when
                captured.
        line_items:
          type: array
          nullable: true
          description: The line items this payment covers.
          items:
            $ref: '#/components/schemas/PaymentProductItem'
        subscription_id:
          type: string
          nullable: true
          description: >-
            The subscription this payment belongs to, or `null` for a one-time
            purchase.
          example: null
        invoice:
          allOf:
            - $ref: '#/components/schemas/PaymentInvoiceInfo'
          nullable: true
          description: >-
            The invoice this payment collected. Present only for subscription
            payments; `null` for one-time purchases.
        refunds:
          type: array
          nullable: true
          description: >-
            IDs of any refunds issued for this payment. `null` if no refund has
            been created.
          items:
            type: string
          example:
            - ref_1a2b3c4d5e
        status_history:
          type: array
          nullable: true
          description: Chronological list of status changes for this payment.
          items:
            type: object
            properties:
              status:
                type: string
                description: Status at this point in time.
                example: succeeded
              occurred_at:
                type: string
                format: date-time
                description: When this status change occurred.
                example: '2026-01-24T14:35:00.000Z'
              reason:
                type: string
                nullable: true
                description: Human-readable reason for the status change, if available.
        created_at:
          type: string
          format: date-time
          description: Creation timestamp.
          example: '2026-04-27T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp.
          example: '2026-04-27T12:00:05Z'
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: Completion timestamp when available.
          example: '2026-04-27T12:00:05Z'
      required:
        - payment_id
        - status
        - amount
        - currency
        - created_at
        - updated_at
    FeeInfo:
      type: object
      description: >-
        The Bachs processing fee on a payment, in the currency it was charged
        in. Taken from the payment with no conversion applied, so it is the
        figure to reconcile against when the payment was not collected in USD.
      properties:
        amount:
          type: string
          description: The fee, as a decimal string in `currency`.
          example: '8.73'
        currency:
          type: string
          description: >-
            The currency the fee was charged in, as an [ISO
            4217](https://en.wikipedia.org/wiki/ISO_4217) code. This is the
            currency the payment was collected in, which is not always the
            currency it settles to.
          example: USD
      required:
        - amount
        - currency
    PaymentProductItem:
      type: object
      description: A product line item purchased in this payment.
      properties:
        product_id:
          type: string
          description: Product identifier.
          example: prod_abc123
        product_name:
          type: string
          description: Product display name.
          example: Premium Plan
        quantity:
          type: integer
          description: Number of units purchased.
          example: 1
        unit_amount:
          type: string
          description: Price per unit in `currency`.
          example: '50.00'
        currency:
          type: string
          description: Currency code for this line item.
          example: USD
        line_total:
          type: string
          description: Total for this line item (`unit_amount` × `quantity`).
          example: '50.00'
      required:
        - product_id
        - product_name
        - quantity
        - unit_amount
        - currency
        - line_total
    PaymentInvoiceInfo:
      type: object
      description: A subscription invoice this payment collected.
      properties:
        invoice_id:
          type: string
          description: The invoice's identifier.
          example: inv_9a8b7c6d5e
        number:
          type: string
          nullable: true
          description: Human-facing invoice number, if assigned.
          example: inv_9a8b7c6d5e
        subscription_id:
          type: string
          nullable: true
          description: The subscription the invoice belongs to.
          example: sub_1a2b3c4d5e
        period_start:
          type: string
          format: date-time
          description: Start of the billing period, UTC.
          example: '2026-04-01T00:00:00Z'
        period_end:
          type: string
          format: date-time
          description: End of the billing period, UTC.
          example: '2026-05-01T00:00:00Z'
        kind:
          type: string
          enum:
            - cycle
            - proration
          description: >-
            `cycle`: a regular subscription-period invoice. `proration`: an
            off-cycle mid-cycle change.
          example: cycle
      required:
        - invoice_id
    Error:
      type: object
      description: Standard error response format used across all API endpoints
      properties:
        detail:
          type: string
          description: Human-readable error message explaining what went wrong.
          example: Invalid request parameters
        error_code:
          type: string
          description: >-
            Machine-readable error code. Use this to handle errors
            programmatically. Common codes: VALIDATION_ERROR, UNAUTHORIZED,
            FORBIDDEN, NOT_FOUND, CONFLICT, TOO_MANY_REQUESTS,
            INTERNAL_SERVER_ERROR, BAD_GATEWAY, SERVICE_UNAVAILABLE.
          example: VALIDATION_ERROR
        errors:
          type: array
          description: >-
            Optional array of field-level validation errors. Only present for
            validation errors (400).
          items:
            type: object
            properties:
              field:
                type: string
                description: The field that failed validation.
                example: amount
              message:
                type: string
                description: Description of the validation failure.
                example: Amount must be a positive decimal string
              type:
                type: string
                description: Error type identifier.
                example: value_error
            required:
              - field
              - message
              - type
      required:
        - detail
        - error_code
  responses:
    BadRequest:
      description: >-
        Bad Request - Validation errors or invalid request format. Check the
        `details` object for field-specific validation errors. Common causes:
        missing required fields, invalid data types, values outside allowed
        ranges, or invalid formats.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: Invalid request parameters
            error_code: VALIDATION_ERROR
            errors:
              - field: amount
                message: Amount must be a positive decimal string
                type: value_error
      headers:
        X-RateLimit-Limit:
          description: Maximum number of requests allowed per minute
          schema:
            type: integer
            example: 100
        X-RateLimit-Remaining:
          description: Number of requests remaining in the current window
          schema:
            type: integer
            example: 95
        X-RateLimit-Reset:
          description: Unix timestamp when the rate limit window resets
          schema:
            type: integer
            example: 1706102400
    Unauthorized:
      description: >-
        Unauthorized - Invalid, missing, or expired API key. Verify your API key
        is correctly formatted and included in the Authorization header as
        `Bearer sk_sandbox_...` or `Bearer sk_live_...`. Check that your key
        hasn't been revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: Invalid API key
            error_code: UNAUTHORIZED
      headers:
        X-RateLimit-Limit:
          description: Maximum number of requests allowed per minute
          schema:
            type: integer
            example: 100
        X-RateLimit-Remaining:
          description: Number of requests remaining in the current window
          schema:
            type: integer
            example: 95
        X-RateLimit-Reset:
          description: Unix timestamp when the rate limit window resets
          schema:
            type: integer
            example: 1706102400
    Forbidden:
      description: >-
        Forbidden - API key does not have permission for the requested resource.
        Verify you're using the correct account's API key and that you're not
        trying to access another account's data.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: API key does not have permission for this operation
            error_code: FORBIDDEN
      headers:
        X-RateLimit-Limit:
          description: Maximum number of requests allowed per minute
          schema:
            type: integer
            example: 100
        X-RateLimit-Remaining:
          description: Number of requests remaining in the current window
          schema:
            type: integer
            example: 95
        X-RateLimit-Reset:
          description: Unix timestamp when the rate limit window resets
          schema:
            type: integer
            example: 1706102400
    NotFound:
      description: >-
        Not Found - The requested resource does not exist. Verify the resource
        ID is correct and that it belongs to your account.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: Resource not found
            error_code: NOT_FOUND
      headers:
        X-RateLimit-Limit:
          description: Maximum number of requests allowed per minute
          schema:
            type: integer
            example: 100
        X-RateLimit-Remaining:
          description: Number of requests remaining in the current window
          schema:
            type: integer
            example: 95
        X-RateLimit-Reset:
          description: Unix timestamp when the rate limit window resets
          schema:
            type: integer
            example: 1706102400
    TooManyRequests:
      description: >-
        Too Many Requests - Rate limit exceeded. Standard tier allows 100
        requests per minute per API key. Wait a few seconds before retrying.
        Check X-RateLimit-Reset header for when the window resets.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: Rate limit exceeded. Please retry after a few seconds.
            error_code: TOO_MANY_REQUESTS
      headers:
        X-RateLimit-Limit:
          description: Maximum number of requests allowed per minute
          schema:
            type: integer
            example: 100
        X-RateLimit-Remaining:
          description: Number of requests remaining in the current window (will be 0)
          schema:
            type: integer
            example: 0
        X-RateLimit-Reset:
          description: Unix timestamp when the rate limit window resets
          schema:
            type: integer
            example: 1706102400
        Retry-After:
          description: Number of seconds to wait before retrying
          schema:
            type: integer
            example: 30
    InternalServerError:
      description: >-
        Internal Server Error - An unexpected error occurred while processing
        the request. Retry with exponential backoff. If the issue persists,
        contact support with your request context.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: An unexpected error occurred. Please try again later.
            error_code: INTERNAL_SERVER_ERROR
      headers:
        X-RateLimit-Limit:
          description: Maximum number of requests allowed per minute
          schema:
            type: integer
            example: 100
        X-RateLimit-Remaining:
          description: Number of requests remaining in the current window
          schema:
            type: integer
            example: 95
        X-RateLimit-Reset:
          description: Unix timestamp when the rate limit window resets
          schema:
            type: integer
            example: 1706102400
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: >-
        Bearer token authentication. Pass your API key as `Authorization: Bearer
        sk_...`. See [Authentication](/authentication) for keys, scopes, and
        sandbox vs production.

````