> ## 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.

# Get payment details



## OpenAPI

````yaml /v2/api-reference/openapi.yaml get /api/payment/{paymentId}
openapi: 3.0.3
info:
  title: Suby.fi Merchant API
  description: >
    API for merchants to integrate Suby.fi payments into their applications.


    ## Quick Start


    1. **Create a product** — define what you sell (one-time or recurring)

    2. **Create a payment or subscription** — generate a checkout link for your
    customer

    3. **Listen for webhooks** — get notified when payments succeed, fail, or
    are refunded


    ## Authentication


    Include your API key in the `X-Suby-Api-Key` header:

    ```

    X-Suby-Api-Key: your_api_key_here

    ```


    Generate your API key from your [merchant dashboard](https://app.suby.fi).


    ## Pricing


    All prices are in **cents** as strings. For example:

    - `"999"` = 9.99 USD/EUR

    - `"1500"` = 15.00 USD/EUR


    ### Custom Pricing


    Products can be created with `isCustomPrice: true` to allow dynamic amounts.

    Instead of a fixed price, the amount is set per-payment via `priceCents`
    when creating a payment or subscription.


    ## Customer Identity


    Every customer has a stable `customerId` (returned by the `GET
    /api/customer` endpoints and on every

    payment/subscription response and webhook). It never changes — **even if the
    customer later changes their

    email** — so it is the reliable key for long-term account history.


    On `POST /api/payment/create` and `POST /api/subscription/create` you can
    identify the customer two ways:


    - **`customerId`** (recommended for returning customers) — links the payment
    to an existing customer by id,
      regardless of their current email. Takes precedence over `customerEmail`. Returns `404` if the id is unknown.
    - **`customerEmail`** — a user account is created (or reused by email) and
    linked to the payment. Use this the
      first time you see a customer; store the `customerId` from the response for next time.
    - **Neither** — the payment is created without a customer. The hosted
    checkout page (`paymentUrl`) prompts the
      buyer for their email at pay time and links the customer then. Webhooks fired **before** that (e.g.
      `CHECKOUT_INITIATED`) have `customerId: null` / `customerEmail: null`; webhooks after a successful payment
      (`CHECKOUT_SUCCESS`, `PAYMENT_SUCCESS`, …) always include both.

    `customerFirstName` / `customerLastName` are optional; they only backfill a
    display name when the customer has

    none, and are ignored when neither `customerId` nor `customerEmail` is
    provided.


    ## Subscription Identity


    A subscription is owned by a **`(customer, product, externalRef)`** triple —
    **not** by customer + product

    alone. This lets a single customer (same `customerId` / email) hold
    **several concurrent subscriptions to the

    same product**, one per distinct `externalRef` (e.g. one per end-user
    account or wallet on your side).


    - **Same `externalRef`** for the same customer + product → the existing
    subscription is reused and **renewed**.

    - **Different `externalRef`** → a **separate** subscription is created. A
    payment made under one `externalRef`
      never extends or reuses a subscription created under another.
    - **No `externalRef`** → all such checkouts for the same customer + product
    share a single subscription (the
      "no external reference" bucket).

    The `externalRef` is set on `POST /api/subscription/create` and is returned
    on the subscription object and on

    every subscription webhook (`data.context.externalRef`).


    **One-time payments** (`POST /api/payment/create`) never create or extend a
    subscription: each call is an

    independent payment with its own `paymentId`, regardless of `externalRef`.


    ## Idempotent Refunds


    `POST /api/refund/{paymentId}` accepts an optional **`Idempotency-Key`**
    header (any unique string you

    generate per refund). If a request times out, retry it with the **same
    key**: the original result is replayed

    (response header `Idempotency-Replayed: true`) instead of issuing the refund
    again. This is essential for

    **partial refunds**, which are otherwise legitimately repeatable. Reusing a
    key with a different request body

    returns `409`. The header is optional; without it, a full refund is still
    naturally idempotent (a second call

    returns a "not refundable" error) but a partial refund is not.


    ## Sandbox Mode


    Create products with `isSandbox: true` to test payment flows without real
    transactions.


    - **Card payments** use test card numbers (see sandbox endpoints in the
    dashboard checkout)

    - **Crypto payments** only support **Base Testnet** (chain ID `84532`). If
    no chains/assets are specified, Base Testnet and its assets are
    auto-selected.

    - **Refunds** on sandbox card payments skip the payment provider and
    directly mark the payment as `REFUNDED`

    - Sandbox payments do **not** create merchant payouts or rolling reserves

    - Webhooks (`CHECKOUT_SUCCESS`, `PAYMENT_SUCCESS`, `PAYMENT_REFUNDED`) are
    still sent normally


    The `isSandbox` field is returned on both product and payment responses.
  version: 2.6.0
  contact:
    email: dev@suby.fi
    url: https://suby.fi
servers:
  - url: https://api.suby.fi
    description: Production server
security:
  - ApiKeyAuth: []
tags:
  - name: Products
    description: >
      Create and manage products. A product defines **what** you sell:

      - **One-time product** — `frequencyInDays` is `null`. Customers pay once.

      - **Subscription product** — `frequencyInDays` is set (e.g. `30` for
      monthly). Customers are billed recurrently.
  - name: Payments - Checkout
    description: |
      Create and track **one-time payments**.
      Use these endpoints for products where `frequencyInDays` is `null`.
  - name: Subscriptions - Checkout
    description: >
      Create, track, and cancel **recurring subscriptions**.

      Use these endpoints for products where `frequencyInDays` is set.


      A subscription is owned by a **`(customer, product, externalRef)`**
      triple: the same customer can hold

      several concurrent subscriptions to the same product by passing a distinct
      `externalRef` on each

      `POST /api/subscription/create`. See **Subscription Identity** in the
      introduction for the full rules.
  - name: Refunds
    description: Refund card (fiat) payments — full or partial
  - name: Discounts
    description: >-
      Create reusable discount codes (percentage or fixed-amount) across one or
      more products
  - name: Customers
    description: Customer information and payment history
  - name: Webhooks
    description: >
      Webhook event format and verification.

      Two event categories are sent:

      - **Payment events** — `CHECKOUT_INITIATED`, `CHECKOUT_SUCCESS`,
      `CHECKOUT_FAILED`, `PAYMENT_SUCCESS`, `PAYMENT_FAILED`,
      `PARTIAL_REFUNDED`, `PAYMENT_REFUNDED`, `PAYMENT_CHARGEBACK`,
      `PAYMENT_SETTLED`

      - **Subscription events** — `SUBSCRIPTION_CREATED`,
      `SUBSCRIPTION_RENEWED`, `SUBSCRIPTION_PAST_DUE`, `SUBSCRIPTION_EXPIRED`


      All webhooks include these headers:

      - `X-Webhook-Event` — the event type

      - `X-Webhook-Timestamp` — Unix timestamp

      - `X-Webhook-Signature` — `v1={hmac_hex}`
paths:
  /api/payment/{paymentId}:
    get:
      tags:
        - Payments - Checkout
      summary: Get payment details
      operationId: getPaymentById
      parameters:
        - name: paymentId
          in: path
          required: true
          schema:
            type: string
          example: pay_abc123
      responses:
        '200':
          description: Payment details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Payment'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          $ref: '#/components/responses/NotFoundError'
components:
  schemas:
    Payment:
      type: object
      properties:
        id:
          type: string
          example: pay_abc123
        txHash:
          type: string
          nullable: true
          description: Blockchain transaction hash (crypto only)
        customerId:
          type: string
          nullable: true
          description: >
            Stable id of the customer linked to the payment. Unchanged even if
            the customer later changes their

            email — use it to reconcile long-term account history. `null` until
            a customer is linked.
          example: usr_abc123
        customerEmail:
          type: string
          format: email
          nullable: true
          description: >
            Email of the customer linked to the payment. `null` when the payment
            was created without a

            `customerEmail` and the customer has not yet completed the hosted
            checkout.
        method:
          type: string
          nullable: true
          enum:
            - CRYPTO
            - CARD
          description: |
            Payment method used:
            - `CRYPTO` — wallet connect or QR code
            - `CARD` — credit/debit card
        tokenAmount:
          type: string
          nullable: true
          description: Amount in token smallest unit (wei/lamports)
        valueUsd:
          type: string
          nullable: true
          description: USD value in cents
        priceCents:
          type: string
          nullable: true
          description: Price in cents (for custom price products)
          example: '999'
        currency:
          type: string
          nullable: true
          enum:
            - USD
            - EUR
          description: Currency of `priceCents`
        status:
          type: string
          enum:
            - INITIATED
            - PENDING
            - PROCESSING
            - CHECKOUT_SUCCESS
            - CHECKOUT_FAILED
            - PAYMENT_SUCCESS
            - TX_SUCCESS
            - TX_EXPIRED
            - FAILED
            - PARTIALLY_REFUNDED
            - REFUNDED
            - CHARGEBACK
          description: >
            - `INITIATED` — payment created, awaiting customer

            - `PENDING` — payment detected, processing

            - `PROCESSING` — crypto transaction being processed

            - `CHECKOUT_SUCCESS` — card checkout authorized

            - `CHECKOUT_FAILED` — card checkout failed before authorization
            (e.g. 3DS failure)

            - `PAYMENT_SUCCESS` — card payment settled

            - `TX_SUCCESS` — crypto transaction confirmed

            - `TX_EXPIRED` — QR code expired

            - `FAILED` — payment failed

            - `PARTIALLY_REFUNDED` — part of the payment has been refunded (card
            only); the remaining amount is `grossAmountCents -
            refundedAmountCents`

            - `REFUNDED` — refunded

            - `CHARGEBACK` — charged back
        subscriptionId:
          type: string
          nullable: true
        asset:
          type: object
          nullable: true
          properties:
            symbol:
              type: string
              example: USDC
            address:
              type: string
            decimals:
              type: integer
              example: 6
            chainId:
              type: integer
              example: 8453
        productId:
          type: string
          nullable: true
        isSandbox:
          type: boolean
          description: '`true` if this payment belongs to a sandbox product'
        metadata:
          type: object
          nullable: true
          additionalProperties: true
        externalRef:
          type: string
          nullable: true
        successUrl:
          type: string
          nullable: true
        cancelUrl:
          type: string
          nullable: true
        grossAmountCents:
          type: integer
          nullable: true
          description: Total amount paid in cents (card only)
        merchantNetCents:
          type: integer
          nullable: true
          description: Net amount for merchant in cents (card only)
        vatAmountCents:
          type: integer
          nullable: true
          description: VAT amount in cents (card only)
        refundedAmountCents:
          type: integer
          nullable: true
          description: >
            Cumulative amount refunded so far, in cents (card only). `0` when
            nothing has been refunded,

            equal to `grossAmountCents` once the payment is fully `REFUNDED`.
            Values in between indicate a

            `PARTIALLY_REFUNDED` payment.
        customFields:
          type: array
          nullable: true
          description: >-
            Custom-field definitions collected at payment creation (`null` if
            none).
          items:
            $ref: '#/components/schemas/CustomField'
        customFieldsResponse:
          type: object
          nullable: true
          additionalProperties: true
          description: >-
            Customer's answers to the payment's `customFields`, keyed by field
            `key` (`null` if none).
        createdAt:
          type: string
          format: date-time
    CustomField:
      type: object
      description: >
        A custom field definition attached to a one-time payment.

        The merchant defines these at payment creation; the customer fills them
        on the checkout page.

        Customer responses are returned in the webhook payload as
        `context.customFieldsResponse`

        (a `{ key: value }` map). Checkbox responses are the literal strings
        `"true"` or `"false"`.
      required:
        - key
        - label
        - type
      properties:
        key:
          type: string
          maxLength: 50
          pattern: ^[a-z0-9_]+$
          description: >
            Unique identifier for the field. Must be unique across the
            `customFields` array on the same payment.

            Lowercase alphanumeric + underscores. Used as the key in
            `customFieldsResponse`.
          example: discord_username
        label:
          type: string
          maxLength: 100
          description: Human-readable label shown on the checkout page.
          example: Discord username
        type:
          type: string
          enum:
            - input
            - select
            - checkbox
          description: >
            - `input` — free-text field (optionally validated via
            `validation.regex`)

            - `select` — dropdown; `options` is required

            - `checkbox` — boolean; stored as `"true"` or `"false"` in the
            response
        required:
          type: boolean
          default: false
          description: >
            When `true`, the customer must fill the field (`input`/`select`) or
            tick it (`checkbox`)

            before submitting the payment. Defaults to `false`.
        placeholder:
          type: string
          maxLength: 200
          description: >
            Optional placeholder shown inside the field (`input`) or as the
            unselected state of the dropdown (`select`).

            Ignored for `type: checkbox`. When omitted, no placeholder is
            rendered.
          example: e.g. test#1234
        options:
          type: array
          description: >-
            **Required for `type: select`.** The list of choices offered to the
            customer.
          items:
            type: object
            required:
              - value
              - label
            properties:
              value:
                type: string
                maxLength: 100
                example: twitter
              label:
                type: string
                maxLength: 100
                example: Twitter / X
        validation:
          type: object
          description: >
            **`type: input` only.** Regex validation applied to the customer's
            input on the checkout page

            (**client-side only**). The server does not re-validate the value
            against `regex`, so this

            is a UX helper — re-check the submitted value in your webhook
            handler if it is

            security-critical.
          properties:
            regex:
              type: string
              maxLength: 500
              description: >-
                Regular expression the customer's input must match (enforced in
                the browser).
              example: ^.{2,32}$
            errorMessage:
              type: string
              maxLength: 200
              description: >-
                Error shown to the customer when the input does not match
                `regex`.
              example: Please enter a valid Discord username
    Error:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: PRODUCT_NOT_FOUND
            message:
              type: string
              example: Product not found
  responses:
    UnauthorizedError:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: UNAUTHORIZED
              message: Invalid or missing API key
    NotFoundError:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: NOT_FOUND
              message: Resource not found
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Suby-Api-Key
      description: API key authentication

````