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

# Cancel a subscription

> Cancels a subscription. The upstream recurring billing (card/Inflow) is always cancelled immediately
so the customer is never charged again.

**Default behaviour — access removal depends on the remaining paid period:**
- **Paid period still running** (`ACTIVE` with a future `expiresAt`) — the cancellation is *scheduled*.
  The subscription stays `ACTIVE` with `cancelAtPeriodEnd: true`, the customer keeps access until
  `currentPeriodEnd` (`expiresAt`), then it moves to `INACTIVE`.
- **Lifetime access (no `expiresAt`) or already-expired paid period** — the subscription is cancelled
  immediately (`status: CANCELLED`) and access is revoked right away.

**Immediate cancellation** — set `immediate: true` (request body) or `?immediate=true` (query string)
to force an instant cancellation even when a paid period is still running: the subscription goes
straight to `CANCELLED`, platform access (Discord/Telegram) is revoked right away, and the customer
loses access immediately (no proration/refund is issued).

Calling this on a subscription that is already `CANCELLED` or already scheduled to cancel
(`cancelAtPeriodEnd: true`) returns a `400`.




## OpenAPI

````yaml /v2/api-reference/openapi.yaml delete /api/subscription/{subscriptionId}
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/subscription/{subscriptionId}:
    delete:
      tags:
        - Subscriptions - Checkout
      summary: Cancel a subscription
      description: >
        Cancels a subscription. The upstream recurring billing (card/Inflow) is
        always cancelled immediately

        so the customer is never charged again.


        **Default behaviour — access removal depends on the remaining paid
        period:**

        - **Paid period still running** (`ACTIVE` with a future `expiresAt`) —
        the cancellation is *scheduled*.
          The subscription stays `ACTIVE` with `cancelAtPeriodEnd: true`, the customer keeps access until
          `currentPeriodEnd` (`expiresAt`), then it moves to `INACTIVE`.
        - **Lifetime access (no `expiresAt`) or already-expired paid period** —
        the subscription is cancelled
          immediately (`status: CANCELLED`) and access is revoked right away.

        **Immediate cancellation** — set `immediate: true` (request body) or
        `?immediate=true` (query string)

        to force an instant cancellation even when a paid period is still
        running: the subscription goes

        straight to `CANCELLED`, platform access (Discord/Telegram) is revoked
        right away, and the customer

        loses access immediately (no proration/refund is issued).


        Calling this on a subscription that is already `CANCELLED` or already
        scheduled to cancel

        (`cancelAtPeriodEnd: true`) returns a `400`.
      operationId: cancelSubscription
      parameters:
        - name: subscriptionId
          in: path
          required: true
          schema:
            type: string
          example: sub_abc123
        - name: immediate
          in: query
          required: false
          description: >-
            Set to `true` to cancel immediately and revoke access now instead of
            scheduling for period end.
          schema:
            type: boolean
            default: false
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                immediate:
                  type: boolean
                  default: false
                  description: >
                    When `true`, cancels immediately and revokes access now
                    (`status: CANCELLED`,

                    `cancelAtPeriodEnd: false`) instead of scheduling the
                    cancellation for the end of the

                    current paid period. Can also be passed as the
                    `?immediate=true` query parameter.
                  example: true
      responses:
        '200':
          description: Subscription cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        example: sub_abc123
                      status:
                        type: string
                        enum:
                          - ACTIVE
                          - CANCELLED
                        description: >
                          `ACTIVE` when the cancellation was scheduled for
                          period end (`cancelAtPeriodEnd: true`),

                          `CANCELLED` when the subscription was cancelled
                          immediately.
                        example: ACTIVE
                      cancelAtPeriodEnd:
                        type: boolean
                        description: >-
                          `true` when access continues until `currentPeriodEnd`;
                          `false` when cancelled immediately.
                        example: true
                      currentPeriodEnd:
                        type: string
                        format: date-time
                        nullable: true
                        description: >-
                          End of the current paid period. `null` for lifetime
                          subscriptions (cancelled immediately).
        '400':
          description: >-
            Subscription is already cancelled or already scheduled to cancel at
            period end
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          $ref: '#/components/responses/NotFoundError'
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

````