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

# Create a payment

> Create a payment and dispatch it to the merchant's configured provider.
Provide either a `productId` **or** an `amount` + `currency`, and exactly
one of `customerId` / `customerData`. Method-specific options are grouped:
card/APM details go under `card`, crypto routing under `crypto`. For
crypto, `method=CRYPTO` requires `crypto.mode`, `crypto.chainId`, and
`crypto.assetId`.

Card flows return a `checkout` handle (redirect / 3DS / client secret);
crypto flows return an `instruction` (QR deposit or wallet-connect calldata).




## OpenAPI

````yaml /v3-beta/api-reference/openapi.yaml post /v3/payments
openapi: 3.1.0
info:
  title: Suby.fi Merchant API
  version: 3.0.0-beta
  description: >
    The **v3** public merchant API for Suby.fi. RESTful (plural,
    resource-oriented

    endpoints under the `/v3` prefix). Authenticate every request

    with your secret API key in the `X-Suby-Api-Key` header.


    - `sk_live_…` keys operate in **production** (real funds).

    - `sk_sandbox_…` keys operate in a fully simulated **sandbox** (test cards,
      Base Sepolia crypto, no real money). The environment is derived from the
      key prefix.

    All monetary amounts are **integer cents** (except `priceCents` on some

    read shapes, which is a string) — token amounts are strings in the smallest

    unit (sats / lamports / wei). Fees are basis points.
  contact:
    email: dev@suby.fi
    url: https://suby.fi
servers:
  - url: https://api.beta.suby.fi
    description: Production + sandbox (environment selected by API key prefix)
security:
  - ApiKeyAuth: []
tags:
  - name: Products
    description: Manage your product catalog (one-time and recurring).
  - name: Checkout
    description: >-
      Hosted checkout — signed session tokens (`cs_…`) plus branding and
      appearance settings.
  - name: Payments
    description: >-
      Create, capture, void, and retrieve one-time & off-session payments (card,
      APM, and crypto).
  - name: Subscriptions
    description: Start and cancel recurring subscriptions.
  - name: Customers
    description: First-class customer records with billing address.
  - name: Payment Methods
    description: >-
      Save a payment method to a customer and manage saved instruments for
      off-session charges.
  - name: Webhook Endpoints
    description: Register outbound webhook destinations and rotate signing secrets.
  - name: Analytics
    description: Org-wide revenue analytics.
  - name: Health
    description: Unauthenticated liveness probe.
paths:
  /v3/payments:
    post:
      tags:
        - Payments
      summary: Create a payment
      description: >
        Create a payment and dispatch it to the merchant's configured provider.

        Provide either a `productId` **or** an `amount` + `currency`, and
        exactly

        one of `customerId` / `customerData`. Method-specific options are
        grouped:

        card/APM details go under `card`, crypto routing under `crypto`. For

        crypto, `method=CRYPTO` requires `crypto.mode`, `crypto.chainId`, and

        `crypto.assetId`.


        Card flows return a `checkout` handle (redirect / 3DS / client secret);

        crypto flows return an `instruction` (QR deposit or wallet-connect
        calldata).
      operationId: createPayment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentBody'
      responses:
        '201':
          description: Payment created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - properties:
                      data:
                        $ref: '#/components/schemas/CreatePaymentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Optional key (≤255 chars, e.g. a UUID v4) that makes this POST safe to
        retry: the first request executes and its response is cached for 24h; a
        retry with the SAME key replays that response instead of re-executing
        (no duplicate payment/subscription). A reused key with a different
        request → `422 IDEMPOTENCY_KEY_CONFLICT`; a retry while the first is
        still in flight → `409`. See the Idempotency guide.
      schema:
        type: string
        maxLength: 255
        example: 5f3b9c2e-1a4d-4f2b-9c31-7e2a1b6d8c04
  schemas:
    CreatePaymentBody:
      type: object
      required:
        - method
      description: >-
        Pricing: provide `productId` (uses the product's price snapshot) OR
        `amount` + `currency` (ad-hoc) — one is required. Customer: exactly one
        of `customerId` (existing) / `customerData` (created on the fly) is
        required — no anonymous charges. Method options are grouped: `card` for
        the card/APM family, `crypto` for `method=CRYPTO`; the block not
        matching `method` is ignored.
      properties:
        productId:
          type: string
          example: pro_abc123
          description: >-
            Charge this product (its priceCents/currency/display are
            snapshotted). Alternative to amount+currency.
        amount:
          type: integer
          minimum: 1
          description: >-
            Ad-hoc amount in cents. Requires `currency`. Alternative to
            productId.
        currency:
          type: string
          pattern: ^[A-Z]{3}$
          description: ISO 4217. Required with `amount`.
        displayName:
          type: string
          maxLength: 200
          description: >-
            Label snapshotted on the Payment (ad-hoc charges). Product-backed
            charges fall back to the product name.
        displayDescription:
          type: string
          maxLength: 500
          nullable: true
        displayImageUrl:
          type: string
          format: uri
          nullable: true
        taxBehavior:
          type: string
          enum:
            - inclusive
            - exclusive
          description: >-
            MoR VAT: `exclusive` (default) adds VAT on top of the price (buyer
            charged price + VAT); `inclusive` treats the price as VAT-inclusive
            (buyer charged the listed price, VAT extracted). Ignored when no VAT
            applies (non-MoR).
        customerId:
          type: string
          example: cus_abc123
          description: >-
            Existing customer. Mutually exclusive with customerData; one is
            required.
        customerData:
          $ref: '#/components/schemas/CustomerData'
        billingAddress:
          $ref: '#/components/schemas/BillingAddress'
          description: >-
            Required by the PSP for card/APM when the stored customer has none
            (Adyen rejects an empty country). Ignored for crypto.
        businessData:
          $ref: '#/components/schemas/BusinessData'
        customerPaymentMethodId:
          type: string
          example: pi_abc123
          description: Charge a saved instrument (pi_…). Required when offSession=true.
        method:
          $ref: '#/components/schemas/PaymentMethodType'
        card:
          $ref: '#/components/schemas/CardOptions'
        crypto:
          $ref: '#/components/schemas/CryptoOptions'
        captureMethod:
          type: string
          enum:
            - automatic
            - manual
          default: automatic
          description: >-
            `automatic` = auth+capture now (purchase). `manual` = authorize only
            → AUTHORIZED; settle later via /capture or release via /void.
            Card/APM only.
        offSession:
          type: boolean
          default: false
          description: >-
            Merchant-initiated debit of a saved instrument (no customer
            present). Requires `customerId` + `customerPaymentMethodId`.
        savePaymentMethod:
          type: boolean
          default: false
          description: >-
            Store the instrument for later off-session debits. Requires
            customerId or customerData.
        successUrl:
          type: string
          format: uri
        cancelUrl:
          type: string
          format: uri
        externalRef:
          type: string
          maxLength: 120
          description: Your reference, stored on the Payment for reconciliation.
        metadata:
          $ref: '#/components/schemas/Metadata'
    SuccessEnvelope:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
        data:
          description: Endpoint-specific payload.
    CreatePaymentResponse:
      type: object
      properties:
        payment:
          $ref: '#/components/schemas/Payment'
        checkout:
          type: object
          nullable: true
          description: Card/APM handoff (mutually exclusive with `instruction`).
          properties:
            providerPaymentId:
              type: string
            redirectUrl:
              type: string
              nullable: true
            threeDsChallengeUrl:
              type: string
              nullable: true
            clientSecret:
              type: string
              nullable: true
        instruction:
          $ref: '#/components/schemas/PaymentInstruction'
    CustomerData:
      type: object
      required:
        - email
      description: >-
        Inline customer for create-on-the-fly. Mutually exclusive with
        `customerId`.
      properties:
        email:
          type: string
          format: email
          maxLength: 320
        firstName:
          type: string
          maxLength: 100
        lastName:
          type: string
          maxLength: 100
    BillingAddress:
      type: object
      properties:
        line1:
          type: string
          maxLength: 200
          nullable: true
        line2:
          type: string
          maxLength: 200
          nullable: true
        city:
          type: string
          maxLength: 120
          nullable: true
        state:
          type: string
          maxLength: 120
          nullable: true
        postalCode:
          type: string
          maxLength: 40
          nullable: true
        country:
          type: string
          description: ISO 3166-1 alpha-2.
          pattern: ^[A-Z]{2}$
          nullable: true
    BusinessData:
      type: object
      description: >-
        B2B purchase details. Accepted on ALL accounts — `businessName` /
        `taxId` are always stored on the customer, and `businessName` doubles as
        the card holder name. The VAT **reverse-charge** it triggers (0% VAT
        except France) only takes effect on Merchant-of-Record accounts; on
        non-MoR accounts no VAT applies at all, so it has no tax effect.
      properties:
        purchaseAsBusiness:
          type: boolean
          description: Marks the purchase as B2B (drives the MoR VAT reverse-charge).
        businessName:
          type: string
          maxLength: 200
          nullable: true
        taxId:
          type: string
          maxLength: 60
          nullable: true
          description: Buyer VAT / fiscal number.
    PaymentMethodType:
      type: string
      enum:
        - CARD
        - APPLE_PAY
        - GOOGLE_PAY
        - KLARNA
        - IDEAL
        - BANCONTACT
        - TWINT
        - BLIK
        - AFFIRM
        - MULTIBANCO
        - PAYPAL
        - SEPA_DIRECT_DEBIT
        - ACH_DIRECT_DEBIT
        - CRYPTO
    CardOptions:
      type: object
      description: >-
        Card / APM instrument options. Ignored on crypto rails. Identify what to
        charge either with a fresh `tokenizedInstrument`, or — for proactive 3DS
        — with the `paymentInstrumentId` + `providerCustomerId` +
        `threedsSessionId` trio returned by the 3DS session.
      properties:
        tokenizedInstrument:
          type: string
          description: Fresh token from the client-side SDK.
        paymentInstrumentId:
          type: string
          description: Proactive 3DS — the already-minted, authenticated instrument.
        providerCustomerId:
          type: string
          description: Proactive 3DS — the customer owning `paymentInstrumentId`.
        threedsSessionId:
          type: string
          description: Proactive 3DS — the resolved 3DS session.
    CryptoOptions:
      type: object
      description: >-
        Crypto routing options. Required when `method=CRYPTO` (`mode`, `chainId`
        and `assetId` are all mandatory then); ignored otherwise.
      properties:
        mode:
          type: string
          enum:
            - qr_deposit
            - wallet_connect
          description: >-
            `qr_deposit` returns a deposit address; `wallet_connect` returns
            calldata to sign.
        chainId:
          type: integer
          minimum: 1
          description: >-
            21=BTC, 101=Solana, else EVM chain id. Bitcoin supports `qr_deposit`
            only.
        assetId:
          type: integer
          minimum: 1
        tokenAmount:
          type: string
          pattern: ^\d+$
          description: >-
            On-chain amount in the smallest unit (sats/lamports/wei). Bypasses
            the spot-price quote — omit to derive it from `amount`+`currency`.
        payerAddress:
          type: string
          minLength: 32
          maxLength: 64
          description: Connected wallet, for `wallet_connect` on Solana/EVM.
    Metadata:
      type: object
      additionalProperties:
        type: string
        nullable: true
      description: |
        Key-value pairs. ≤50 keys, keys ≤40 chars, values are
        strings ≤500 chars (or `null` to clear). Nested structures must be
        JSON-stringified into a single string value.
    Payment:
      type: object
      description: Public payment shape (`PaymentPublic`).
      properties:
        id:
          type: string
          example: pay_abc123
        organizationId:
          type: string
        customerId:
          type: string
          nullable: true
        productId:
          type: string
          nullable: true
        subscriptionId:
          type: string
          nullable: true
        status:
          $ref: '#/components/schemas/PaymentStatus'
        declineCode:
          type: string
          nullable: true
          description: >-
            Normalized decline reason for a FAILED charge (Stripe-parity
            `decline_code`), e.g. `insufficient_funds`, `not_supported`,
            `stolen_card`, `expired_card`. Null on non-declined / non-card
            payments.
        declineCategory:
          type: string
          nullable: true
          enum:
            - SOFT
            - HARD
          description: >-
            Retryability of the decline: SOFT = transient (a retry may succeed),
            HARD = terminal (the credential is unusable). Null when not a
            decline.
        displayName:
          type: string
          nullable: true
        displayDescription:
          type: string
          nullable: true
        displayImageUrl:
          type: string
          nullable: true
        priceCents:
          type: string
          nullable: true
        currency:
          type: string
          nullable: true
        tokenAmount:
          type: string
          nullable: true
        tokenFeeAmount:
          type: string
          nullable: true
        quoteFiatAmountCents:
          type: integer
          nullable: true
        quoteFiatCurrency:
          type: string
          nullable: true
        grossAmountCents:
          type: integer
          nullable: true
        platformFeeCents:
          type: integer
          nullable: true
        merchantNetCents:
          type: integer
          nullable: true
        vatAmountCents:
          type: integer
          nullable: true
        vatRateBps:
          type: integer
          nullable: true
          description: >-
            Applied VAT rate in basis points (2000 = 20%); MoR + EU only, else
            null
        taxInclusive:
          type: boolean
          description: >-
            true = price is VAT-inclusive (gross = price); false = exclusive
            (gross = price + VAT)
        refundedAmountCents:
          type: integer
          nullable: true
        refundedAt:
          type: string
          format: date-time
          nullable: true
        cryptoSettlementMode:
          type: string
          nullable: true
        settlementChainId:
          type: integer
          nullable: true
        settlementAsset:
          type: string
          nullable: true
        settlementRecipient:
          type: string
          nullable: true
        depositAddress:
          type: string
          nullable: true
        settlementTxHash:
          type: string
          nullable: true
        successUrl:
          type: string
          nullable: true
        cancelUrl:
          type: string
          nullable: true
        externalRef:
          type: string
          nullable: true
        paymentReceivedAt:
          type: string
          format: date-time
          nullable: true
        paymentSettledAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            When an unfinished intent stops being payable. Set for card/APM
            (bounded to the checkout-session window, else 30 min) and crypto
            (per-chain deposit window). Past this, an abandoned PENDING intent
            is swept to EXPIRED. Null only for legacy rows.
        statusTimeline:
          type: array
          description: >-
            Append-only status history — every status the payment moved through,
            oldest→newest. The last entry is where it currently stands /
            stopped. Populated on detail + list reads; empty for legacy rows
            created before the feature.
          items:
            type: object
            properties:
              status:
                type: string
                description: The status the payment transitioned into.
                enum:
                  - PENDING
                  - AUTHORIZED
                  - PENDING_3DS
                  - PENDING_REDIRECT
                  - BRIDGING
                  - RECEIVED
                  - PARTIAL
                  - COMPLETED
                  - FAILED
                  - DECLINED
                  - REFUNDED
                  - PARTIALLY_REFUNDED
                  - CANCELED
                  - EXPIRED
              at:
                type: string
                format: date-time
                description: When the transition happened.
        asset:
          type: object
          nullable: true
          description: Populated on crypto reads.
          properties:
            chainId:
              type: integer
            symbol:
              type: string
            decimals:
              type: integer
            address:
              type: string
              description: Token contract / SPL mint, or `NATIVE`.
        deposit:
          type: object
          nullable: true
          description: Populated on crypto reads while a deposit is being watched.
          properties:
            expected:
              type: string
            received:
              type: string
            remaining:
              type: string
            confirmations:
              type: object
              nullable: true
              properties:
                observed:
                  type: integer
                required:
                  type: integer
                detectedAt:
                  type: string
                  format: date-time
            warnings:
              type: array
              items:
                type: object
                properties:
                  code:
                    type: string
                    enum:
                      - INSUFFICIENT_DEPOSIT_AMOUNT
                      - RBF_DETECTED
                      - NETWORK_FEE_SURCHARGE
                  message:
                    type: string
                  data:
                    type: object
                    additionalProperties: true
    PaymentInstruction:
      description: >-
        Crypto handoff (mutually exclusive with `checkout`), discriminated on
        `kind`.
      oneOf:
        - type: object
          properties:
            kind:
              type: string
              enum:
                - qr_deposit
            chainId:
              type: integer
            depositAddress:
              type: string
            expectedAmount:
              type: string
            decimals:
              type: integer
            symbol:
              type: string
            address:
              type: string
              description: Token contract / SPL mint, or `NATIVE`.
            bip21Uri:
              type: string
              nullable: true
            surchargeNote:
              type: string
              nullable: true
        - type: object
          properties:
            kind:
              type: string
              enum:
                - wallet_connect
            chainId:
              type: integer
            contractAddress:
              type: string
            calldata:
              type: string
            value:
              type: string
              nullable: true
            address:
              type: string
            blockhashExpiresAt:
              type: string
              nullable: true
            gasLimit:
              type: string
              nullable: true
            approval:
              type: object
              nullable: true
              properties:
                spender:
                  type: string
                tokenAddress:
                  type: string
                amount:
                  type: string
                calldata:
                  type: string
    Error:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          description: Machine-readable error code.
          example: NOT_FOUND
        message:
          type: string
          example: Resource not found
        data:
          description: Optional error detail.
    ValidationErrorEnvelope:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            data:
              type: object
              properties:
                fieldErrors:
                  type: array
                  items:
                    type: object
                    properties:
                      field:
                        type: string
                      message:
                        type: string
    PaymentStatus:
      type: string
      enum:
        - PENDING
        - AUTHORIZED
        - PENDING_3DS
        - PENDING_REDIRECT
        - BRIDGING
        - RECEIVED
        - PARTIAL
        - COMPLETED
        - FAILED
        - DECLINED
        - REFUNDED
        - PARTIALLY_REFUNDED
        - CANCELED
        - EXPIRED
  responses:
    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error: UNAUTHORIZED
            message: Invalid or missing API key
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error: NOT_FOUND
            message: Resource not found
    ValidationError:
      description: Request failed schema validation
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorEnvelope'
          example:
            success: false
            error: VALIDATION_ERROR
            message: Validation failed
            data:
              fieldErrors:
                - field: method
                  message: method is a required field
    RateLimited:
      description: Too many requests
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error: RATE_LIMITED
            message: Too many requests
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Suby-Api-Key
      description: Secret API key. `sk_live_…` (production) or `sk_sandbox_…` (sandbox).

````