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

# Refund a card payment (full or partial)

> Refunds a card (fiat) payment.
Only payments with status `PAYMENT_SUCCESS`, `CHECKOUT_SUCCESS` (or `PARTIALLY_REFUNDED`, for a further partial) and source `FIAT` can be refunded.
Crypto payments cannot be refunded from API.

### Full vs partial refund

- **Omit `amountInCents`** → the full remaining amount is refunded and the payment becomes `REFUNDED`.
- **Provide `amountInCents`** → only that amount (in cents, in the payment's currency) is refunded.
  You may call the endpoint multiple times as long as the cumulative refunded amount does not exceed the
  captured total (`grossAmountCents`). While a residual amount remains, the payment is `PARTIALLY_REFUNDED`;
  once the cumulative refunds reach the captured total it flips to `REFUNDED`.

The cumulative amount refunded so far is exposed as `refundedAmountCents` on the payment, and each call
returns the `remainingRefundableCents` still available.

**Webhooks:** a partial refund emits `PARTIAL_REFUNDED`; the refund that completes the full amount (or a plain full refund) emits `PAYMENT_REFUNDED`.

**Sandbox mode:** For sandbox product payments, the refund skips the payment provider call and directly updates the payment status.




## OpenAPI

````yaml /v2/api-reference/openapi.yaml post /api/refund/{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/refund/{paymentId}:
    post:
      tags:
        - Refunds
      summary: Refund a card payment (full or partial)
      description: >
        Refunds a card (fiat) payment.

        Only payments with status `PAYMENT_SUCCESS`, `CHECKOUT_SUCCESS` (or
        `PARTIALLY_REFUNDED`, for a further partial) and source `FIAT` can be
        refunded.

        Crypto payments cannot be refunded from API.


        ### Full vs partial refund


        - **Omit `amountInCents`** → the full remaining amount is refunded and
        the payment becomes `REFUNDED`.

        - **Provide `amountInCents`** → only that amount (in cents, in the
        payment's currency) is refunded.
          You may call the endpoint multiple times as long as the cumulative refunded amount does not exceed the
          captured total (`grossAmountCents`). While a residual amount remains, the payment is `PARTIALLY_REFUNDED`;
          once the cumulative refunds reach the captured total it flips to `REFUNDED`.

        The cumulative amount refunded so far is exposed as
        `refundedAmountCents` on the payment, and each call

        returns the `remainingRefundableCents` still available.


        **Webhooks:** a partial refund emits `PARTIAL_REFUNDED`; the refund that
        completes the full amount (or a plain full refund) emits
        `PAYMENT_REFUNDED`.


        **Sandbox mode:** For sandbox product payments, the refund skips the
        payment provider call and directly updates the payment status.
      operationId: refundPayment
      parameters:
        - name: paymentId
          in: path
          required: true
          schema:
            type: string
          example: pay_abc123
        - name: Idempotency-Key
          in: header
          required: false
          description: >
            Optional. A unique string you generate per refund. Retrying a
            request with the **same key** replays the

            original result (response header `Idempotency-Replayed: true`)
            instead of issuing the refund again — making

            a timed-out request safe to retry. Reusing a key with a different
            request body returns `409`.

            Especially important for **partial refunds**, which are otherwise
            legitimately repeatable.
          schema:
            type: string
            maxLength: 255
          example: refund-pay_abc123-01HZ...
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 500
                  description: Reason for the refund
                  example: Customer requested refund
                amountInCents:
                  type: integer
                  minimum: 1
                  description: >
                    Optional. Amount to refund in cents, in the payment's
                    currency.

                    Omit to refund the full remaining amount. When provided,
                    must not exceed the remaining

                    refundable amount (`grossAmountCents -
                    refundedAmountCents`).
                  example: 500
                idempotencyKey:
                  type: string
                  maxLength: 255
                  description: >
                    Optional alternative to the `Idempotency-Key` header (the
                    header takes precedence). Same

                    behavior — retrying with the same value replays the original
                    result instead of re-refunding.
                  example: refund-pay_abc123-01HZ...
      responses:
        '200':
          description: >-
            Refund initiated (or the stored result replayed for a repeated
            `Idempotency-Key`).
          headers:
            Idempotency-Replayed:
              description: >-
                Present and set to `true` when this response was replayed from a
                prior request carrying the same `Idempotency-Key` (the refund
                was not issued again).
              schema:
                type: string
                example: 'true'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      paymentId:
                        type: string
                        example: pay_abc123
                      status:
                        type: string
                        enum:
                          - PARTIALLY_REFUNDED
                          - REFUNDED
                        example: PARTIALLY_REFUNDED
                      refundedAmountCents:
                        type: integer
                        description: >-
                          Cumulative amount refunded so far, in cents. Only
                          returned for partial refunds.
                        example: 500
                      remainingRefundableCents:
                        type: integer
                        description: >-
                          Amount still available to refund, in cents. Only
                          returned for partial refunds.
                        example: 500
                      feeDebt:
                        type: string
                        nullable: true
                        description: >
                          Outstanding provider-fee debt carried on the merchant
                          account after the refund,

                          in cents as a string. `null`/absent when there is no
                          residual fee to reconcile.
        '400':
          description: >-
            Payment cannot be refunded (e.g. amount exceeds remaining refundable
            amount)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '409':
          description: >
            Idempotency conflict — either the `Idempotency-Key` was reused with
            a different request body

            (`IDEMPOTENCY_KEY_CONFLICT`), or a request with the same key is
            still being processed

            (`IDEMPOTENCY_REQUEST_IN_PROGRESS`, retry shortly).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    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

````