# Products

Create and manage reusable hosted checkout products your customers pay with stablecoins or PIX.

A **product** is a reusable, sellable catalog item. Each product has a hosted page at `https://dolafy.com/checkout/<slug>` where your customer completes payment. You settle in stablecoin; your customer pays with a stablecoin or with PIX.

Create **one product per catalog item**, then either:

- send buyers to `product.checkoutUrl`, appending your own attribution (for example `?userID=user_42&txn_id=txn_1048`), or
- create a PIX charge with [`POST /v1/checkout/sessions`](/docs/checkout-sessions) for a transparent checkout.

Do **not** create a product per buyer, order, or payment attempt. All checkout endpoints are scoped to the organization behind your API key.

## The product object

```json
{
  "id": "6a713a92-350a-4cc6-a996-1e55e97d3a36",
  "externalProductId": "pro-plan",
  "slug": "c-8f41a2d9",
  "name": "Pro Plan",
  "description": "Monthly access to the Pro plan.",
  "amount": "29.00",
  "currency": "USD",
  "status": "active",
  "checkoutUrl": "https://dolafy.com/checkout/c-8f41a2d9",
  "imageUrl": "https://example.com/pro-plan.png",
  "redirectUrlSuccess": "https://example.com/checkout/success",
  "redirectUrlCancel": "https://example.com/checkout/cancel",
  "paymentMethods": [
    { "type": "crypto", "chain": "base", "currency": "USD", "settlementCurrency": "USDC" }
  ],
  "createdAt": "2026-07-04T12:00:00.000Z",
  "updatedAt": "2026-07-04T12:00:00.000Z"
}
```

- `externalProductId` is your own optional identifier, unique within your organization. It can
  identify a product when creating a checkout session and can be found through the list search.
  Product GET, PATCH, and DELETE paths use the Dolafy `id`.
- `slug`, settlement destination, tracking pixels, and internal metadata are **not** writable through this API.

## List products

```http
GET /v1/checkout/products
x-api-key: dlfy_live_...
```

| Parameter | Default | Description |
|-----------|---------|-------------|
| `status` | `active` | `active`, `archived`, or `all`. |
| `q` | — | Searches name, Dolafy id, slug, and `externalProductId` (≤ 200 chars). |
| `limit` | `50` | 1–100. |
| `offset` | `0` | Zero or greater. |

Returns `{ "products": [ ... ], "pagination": { "limit", "offset", "hasMore" } }`, newest first.
Omitting `status` returns active products only. Archived products appear only when explicitly
requesting `status=archived` or `status=all`.

## Get a product

```http
GET /v1/checkout/products/:productId
x-api-key: dlfy_live_...
```

Returns one active or archived product in the `{ "product": ... }` shape. Unknown or cross-organization ids return `404 product_not_found`.
Use the exact `product.id` returned by product creation or the product list. The hosted-page `slug`
and `externalProductId` are not accepted in this path.

## Create a product

A product is one of two types, chosen by `paymentMethods`:

- a **stablecoin** product — USD-denominated, paid in USDC/USDT on Base/Solana (shown below), or
- a **PIX** product — BRL-denominated, paid with PIX (see [Create a PIX product](#create-a-pix-product)).

If `paymentMethods` is omitted, the product follows your organization's default checkout settings.

```http
POST /v1/checkout/products
x-api-key: dlfy_live_...
Idempotency-Key: catalog:pro-plan:v1
content-type: application/json
```

`Idempotency-Key` is **required** (8–200 letters, numbers, dots, underscores, colons, or hyphens). The example below creates a stablecoin product.

### Request

```json
{
  "externalProductId": "pro-plan",
  "name": "Pro Plan",
  "description": "Monthly access to the Pro plan.",
  "amount": "29.00",
  "imageUrl": "https://example.com/pro-plan.png",
  "redirectUrlSuccess": "https://example.com/checkout/success",
  "redirectUrlCancel": "https://example.com/checkout/cancel",
  "paymentMethods": {
    "networks": ["base"],
    "tokens": ["usdc"]
  }
}
```

Validation:

- `name` is 3–120 characters; `description` ≤ 500; URLs must be HTTP/HTTPS; `amount` must be positive.
- `externalProductId` uses the same identifier characters, ≤ 200, and must be unique in your organization.
- An explicit stablecoin override must keep **Base and USDC enabled**, otherwise `400 base_required` or `400 usdc_required`. Unknown networks/tokens return `400 unsupported_checkout_network` or `400 unsupported_checkout_token`.

Returns `201 Created` (including an identical idempotent replay) with the `{ "product": ... }` shape.

- Reusing an idempotency key with a **different** body returns `409 idempotency_key_conflict`.
- An `externalProductId` collision returns `409 external_product_id_conflict`.

### Create a PIX product

A PIX product is created with the **same** `POST /v1/checkout/products` endpoint. There are only two differences from a stablecoin product:

1. Set `paymentMethods` to exactly `{ "pix": true }`.
2. `amount` is the price in **BRL** (Brazilian reais), not USD.

That's it — don't send `networks` or `tokens`. Here is a full request and response:

```http
POST /v1/checkout/products
x-api-key: dlfy_live_...
Idempotency-Key: catalog:curso-pro:v1
content-type: application/json
```

```json
{
  "externalProductId": "curso-pro",
  "name": "Curso Pro",
  "description": "Acesso ao Curso Pro.",
  "amount": "49.90",
  "redirectUrlSuccess": "https://example.com/checkout/success",
  "paymentMethods": { "pix": true }
}
```

Response — note `currency` is `BRL` and `paymentMethods` has a single `bank`/`pix` entry (compare with the `crypto` entry a stablecoin product returns):

```json
{
  "product": {
    "id": "8b2c9f14-6d3a-4e77-9a21-5c0e1f7b2d40",
    "externalProductId": "curso-pro",
    "slug": "c-1a2b3c4d",
    "name": "Curso Pro",
    "description": "Acesso ao Curso Pro.",
    "amount": "49.90",
    "currency": "BRL",
    "status": "active",
    "checkoutUrl": "https://dolafy.com/checkout/c-1a2b3c4d",
    "imageUrl": null,
    "redirectUrlSuccess": "https://example.com/checkout/success",
    "redirectUrlCancel": null,
    "paymentMethods": [
      { "type": "bank", "currency": "BRL", "rails": ["pix"], "label": "BRL Pix" }
    ],
    "createdAt": "2026-07-22T12:00:00.000Z",
    "updatedAt": "2026-07-22T12:00:00.000Z"
  }
}
```

Rules for PIX products:

- `pix` cannot be combined with `networks`, `tokens`, or `inheritOrganizationSettings` — invalid combinations return `400 invalid_payment_methods`.
- Your organization must have a live PIX route and an active BRL account, otherwise creation returns `503 checkout_pix_not_available`.
- Buyers pay this product with PIX only. To generate a payable PIX QR code / Copia e Cola for a specific order, create a [session](/docs/checkout-sessions) from this product.

## Edit a product

```http
PATCH /v1/checkout/products/:productId
content-type: application/json
```

PATCH is partial — omitted fields keep their values; an empty body returns `400 invalid_body`. Editable fields: `externalProductId` (string or `null`), `name`, `amount`, `description`, `imageUrl`, `redirectUrlSuccess`, `redirectUrlCancel`, `status` (`active` or `archived`), and `paymentMethods`.

```json
{
  "name": "Pro Plan Annual",
  "amount": "290.00",
  "status": "active"
}
```

Returns `200 OK` with the `{ "product": ... }` shape. To move a product between PIX and stablecoins, see [Switching a product between rails](#switching-a-product-between-rails).

## Archive a product

```http
DELETE /v1/checkout/products/:productId
```

Idempotently **soft-archives** a product — nothing is deleted. An archived product disappears from the default list and stops accepting new hosted charges and new API sessions, but existing charges and previously created session URLs remain readable and payable until their normal expiry. Restore with `PATCH { "status": "active" }`. Returns `200 OK` with the product shape and `status: "archived"`.

## Hosted checkout URL parameters

When you send buyers to `product.checkoutUrl`, you can prefill context and carry attribution:

- `userID` (or `userId`, `user_id`) — shown as a read-only **Order reference** and persisted as the charge `externalId`.
- `userEmail` (or `email`) — prefills the buyer email field.
- Any other query parameter is passthrough attribution: included in charge metadata and appended to your success redirect after payment.

```text
https://dolafy.com/checkout/gold-access?userID=player_123&userEmail=buyer@example.com&campaign=launch
```

Discounts are **not** available on hosted product URLs — a query parameter cannot lower the price. To charge a one-off discounted amount, create a [session](/docs/checkout-sessions) with `discountPercent`.

## Advanced

### Switching a product between rails

`paymentMethods` is editable with [`PATCH`](#edit-a-product):

- Use `paymentMethods.inheritOrganizationSettings: true` to clear a stablecoin override and follow your organization's default checkout settings.
- Convert a PIX product back to stablecoins by sending `networks`/`tokens`, or by inheriting organization settings; its currency returns to USD.
- **Switching rails changes the currency label but does not convert the numeric price** — a `49.90` BRL product becomes a `49.90` USD product. Include `amount` in the same PATCH when the new rail needs a different price.

```json
{
  "amount": "29.00",
  "paymentMethods": { "inheritOrganizationSettings": true }
}
```

Next: create a transparent PIX charge with [Sessions](/docs/checkout-sessions), read results with [Transactions](/docs/checkout-transactions), or get notified via [Webhooks](/docs/checkout-webhooks).
