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

> Creates a new product. Set `frequencyInDays` to define the billing cycle:
- Omit or set to `null` → **one-time** product
- Set to `30` → **monthly** subscription, `365` → yearly, etc.

### Custom pricing (`isCustomPrice: true`)

When enabled, the product has **no fixed price**. Instead, you pass `priceCents` and `currency` each time you create a payment or subscription.

**One-time + custom price** — ideal for variable amounts:
- Wallet top-ups (user chooses how much to add)
- Donations or tips
- Invoices with different totals

**Subscription + custom price** — ideal when the price is decided at checkout:
- Tiered pricing (e.g. user picks a seat count or plan level)
- Negotiated rates set per-customer

> When `isCustomPrice` is `true`, do **not** set `priceCents` or `currency` on the product — they are provided per-payment via `POST /api/payment/create` or `POST /api/subscription/create`.

**Platform:** Only `WEB` and `INVOICE` are supported via the API.




## OpenAPI

````yaml /v2/api-reference/openapi.yaml post /api/product/create
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/product/create:
    post:
      tags:
        - Products
      summary: Create a product
      description: >
        Creates a new product. Set `frequencyInDays` to define the billing
        cycle:

        - Omit or set to `null` → **one-time** product

        - Set to `30` → **monthly** subscription, `365` → yearly, etc.


        ### Custom pricing (`isCustomPrice: true`)


        When enabled, the product has **no fixed price**. Instead, you pass
        `priceCents` and `currency` each time you create a payment or
        subscription.


        **One-time + custom price** — ideal for variable amounts:

        - Wallet top-ups (user chooses how much to add)

        - Donations or tips

        - Invoices with different totals


        **Subscription + custom price** — ideal when the price is decided at
        checkout:

        - Tiered pricing (e.g. user picks a seat count or plan level)

        - Negotiated rates set per-customer


        > When `isCustomPrice` is `true`, do **not** set `priceCents` or
        `currency` on the product — they are provided per-payment via `POST
        /api/payment/create` or `POST /api/subscription/create`.


        **Platform:** Only `WEB` and `INVOICE` are supported via the API.
      operationId: createProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - paymentMethods
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 50
                  description: Product display name
                  example: Premium Access
                description:
                  type: string
                  maxLength: 200
                  description: Product description visible to customers
                  example: Monthly premium membership
                frequencyInDays:
                  type: integer
                  nullable: true
                  minimum: 1
                  description: >
                    Billing cycle in days. Omit or set to `null` for one-time
                    products.

                    Common values: `7` (weekly), `30` (monthly), `90`
                    (quarterly), `365` (yearly)
                  example: 30
                isSandbox:
                  type: boolean
                  default: false
                  description: >
                    Set to `true` to create a sandbox (test) product.

                    Sandbox products only support Base Testnet (`84532`) for
                    crypto.

                    If no chains/assets are specified, Base Testnet and its
                    active assets are auto-selected.

                    Card payments use test card numbers instead of real
                    transactions.
                  example: false
                isCustomPrice:
                  type: boolean
                  default: false
                  description: >
                    Set to `true` for dynamic pricing. When enabled,
                    `priceCents` must NOT be set on the product.

                    Instead, the price is provided per-payment via `priceCents`
                    when creating a payment or subscription.
                  example: false
                priceCents:
                  type: string
                  description: >
                    Price in cents as a string. **Required** when
                    `isCustomPrice` is `false` (default).

                    **Must NOT be provided** when `isCustomPrice` is `true`.

                    For `CARD` payments, minimum is `"200"` (2.00 USD/EUR).
                  example: '999'
                currency:
                  type: string
                  enum:
                    - USD
                    - EUR
                  description: >
                    Price currency. **Required** when `isCustomPrice` is `false`
                    (default).

                    **Must NOT be provided** when `isCustomPrice` is `true`.
                  example: EUR
                platform:
                  type: string
                  enum:
                    - WEB
                    - INVOICE
                  default: WEB
                  description: Target platform. Defaults to `WEB`
                  example: WEB
                paymentMethods:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum:
                      - CRYPTO
                      - CARD
                  description: >
                    Payment methods to enable:

                    - `CRYPTO` — wallet connect and QR code (requires merchant
                    receiving address)

                    - `CARD` — credit/debit card (requires merchant verification
                    + payout address)
                  example:
                    - CRYPTO
                    - CARD
                acceptedAssets:
                  type: array
                  items:
                    type: string
                  description: >
                    **Crypto only.** Ignored when `paymentMethods` is `["CARD"]`
                    only.

                    Token symbols to accept (e.g. `["USDC", "USDT"]`).

                    Automatically resolved across all accepted chains.

                    If omitted, all active assets are enabled.
                  example:
                    - USDC
                    - USDT
                acceptedChains:
                  type: array
                  items:
                    type: integer
                  description: >
                    **Crypto only.** Ignored when `paymentMethods` is `["CARD"]`
                    only.

                    Blockchain chain IDs to accept.

                    Common IDs: `8453` (Base), `42161` (Arbitrum), `1`
                    (Ethereum), `56` (BSC), `101` (Solana).

                    If omitted, all active chains matching your merchant
                    addresses are enabled.
                  example:
                    - 8453
                    - 42161
                supply:
                  type: integer
                  minimum: 1
                  description: >-
                    Maximum number of subscriptions or one-time purchases
                    available. Omit for unlimited.
                  example: 100
                imageUrl:
                  type: string
                  format: uri
                  description: Product image URL displayed on checkout
                  example: https://example.com/product.png
      responses:
        '201':
          description: Product created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Product'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
components:
  schemas:
    Product:
      type: object
      properties:
        id:
          type: string
          example: pro_abc123
        name:
          type: string
          example: Premium Access
        description:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - ACTIVE
            - CANCELLED
            - DRAFT
        platform:
          type: string
          enum:
            - WEB
            - INVOICE
            - DISCORD
            - TELEGRAM
        frequencyInDays:
          type: integer
          nullable: true
          description: '`null` = one-time, `30` = monthly, `365` = yearly, etc.'
        isSandbox:
          type: boolean
          description: '`true` if this is a sandbox (test) product'
        isCustomPrice:
          type: boolean
          description: '`true` if the price is set per-payment instead of on the product'
        priceCents:
          type: string
          nullable: true
          description: Price in cents. `null` for custom price products.
          example: '999'
        currency:
          type: string
          nullable: true
          enum:
            - USD
            - EUR
          description: '`null` for custom price products'
        supply:
          type: integer
        imageUrl:
          type: string
          nullable: true
          format: uri
        createdAt:
          type: string
          format: date-time
        paymentMethods:
          type: array
          items:
            type: string
            enum:
              - CRYPTO
              - CARD
        acceptedAssets:
          type: array
          description: >-
            Accepted crypto assets. Only present when the product accepts crypto
            and has assets configured — omitted otherwise.
          items:
            type: object
            properties:
              symbol:
                type: string
              decimals:
                type: integer
        acceptedChains:
          type: array
          description: >-
            Accepted crypto chains. Only present when the product accepts crypto
            and has chains configured — omitted otherwise.
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
    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
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Suby-Api-Key
      description: API key authentication

````