> ## Documentation Index
> Fetch the complete documentation index at: https://docs.custral.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Products

> List, create and change the products in your catalog. Each one is a record in your Products object, and Custral pushes it to Stripe.

A product is a record in the object picked as **Products** in
**Settings → Payments**. Creating or changing one through these endpoints is the
same as editing that record: the fields are written through the object's own
mapping, and Custral pushes the price to the workspace's Stripe account.

Prices are in **major** units (`49.5` is \$49.50). `unitAmount` beside them is
the same price in minor units, the way Stripe stores it.

| Endpoint | Scope | Does |
| - | - | - |
| `GET /v1/products` | `billing:read` | Lists the catalog |
| `GET /v1/products/{product}` | `billing:read` | One product |
| `POST /v1/products` | `billing:admin` | Creates a product |
| `PATCH /v1/products/{product}` | `billing:admin` | Changes the named fields |
| `POST /v1/products/{product}/archive` | `billing:admin` | Takes a product off the catalog |

`{product}` is the product's id or its exact name. Writes need a key whose
creator is a workspace admin. Creating a product charges nobody.

## The Product object

<ResponseExample>
  ```json A product theme={null}
  {
    "id": "rec_…",
    "name": "Pro seat",
    "description": "One seat on the Pro plan",
    "price": 49,
    "unitAmount": 4900,
    "currency": "usd",
    "interval": "month",
    "usageEvent": null,
    "stripe": "synced",
    "stripeAmount": 4900
  }
  ```
</ResponseExample>

<ResponseField name="interval" type="string">
  `month`, `year` or `one_time`.
</ResponseField>

<ResponseField name="usageEvent" type="string | null">
  The usage event a metered product bills by.
</ResponseField>

<ResponseField name="stripe" type="string">
  Whether Stripe has the price the catalog shows: `synced`, `pending` (the push
  hasn't finished), `out_of_sync` (Stripe charges a different amount) or
  `unknown` (Stripe couldn't be read).
</ResponseField>

<ResponseField name="stripeAmount" type="integer | null">
  What Stripe charges, in minor units, when it differs or is known.
</ResponseField>

## Create a product

`POST /v1/products`

```ts theme={null}
const seat = await custral.products.create({
  name: "Pro seat",
  price: 49,
  currency: "usd",
  interval: "month",
});
```

<ParamField body="name" type="string" required>
  Up to 200 characters.
</ParamField>

<ParamField body="description" type="string">
  Up to 2,000 characters.
</ParamField>

<ParamField body="price" type="number">
  Per unit, in major units.
</ParamField>

<ParamField body="currency" type="string">
  An ISO code such as `usd`.
</ParamField>

<ParamField body="interval" type="string">
  `month`, `year` or `one_time`. Matched against the options on your Products
  object.
</ParamField>

<ParamField body="usageEvent" type="string">
  For a metered product: the usage event it bills by.
</ParamField>

## Change a product

`PATCH /v1/products/{product}` takes the same fields, all optional. Omitted
fields stay as they are.

```ts theme={null}
await custral.products.update({product: "Pro seat", price: 59});
```

Stripe prices can't be edited, so a new price is pushed as a new Stripe price
and the old one is deactivated. Existing subscriptions keep the price they were
sold until they change plan.

## Archive a product

`POST /v1/products/{product}/archive` takes the product off the catalog and
returns `{archived: true}`. Quotes already made and Stripe subscriptions already
running keep what they had.

## Errors

| Code | When |
| - | - |
| `products_object_not_mapped` | No Products object is picked in Settings → Payments. |
| `product_field_not_mapped` | The Products object has no field mapped for one you sent. Map it in Settings → Payments. |
| `product_option_not_found` | `interval` or `currency` has no matching option on that field. The message lists the options. |
| `product_not_found` | No product has that id or name. |
| `product_ambiguous` | More than one product has that name. Use its id. |
| `insufficient_permissions` | A write, and the key's creator is not a workspace admin. |

See [Errors](/dev/errors/overview) for the HTTP status each code maps to.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.