---
title: "Products"
description: "Keep what you sell as Products with prices, then archive, delete or search them."
url: https://docs.fluveo.com/catalog/products/
---

> Index of every page: https://docs.fluveo.com/llms.txt. Integrating Fluveo? Read https://docs.fluveo.com/agents/integrate.md first.

> Too long? This page with code in one language only: https://docs.fluveo.com/catalog/products.curl.md (cURL), https://docs.fluveo.com/catalog/products.node.md (Node.js), https://docs.fluveo.com/catalog/products.python.md (Python), https://docs.fluveo.com/catalog/products.php.md (PHP).

# Products

Keep what you sell as Products with prices, then archive, delete or search them.

A Product is something you sell: an item, a service. On its own it has no amount. You give it one or more
[Prices](https://docs.fluveo.com/catalog/prices.md), and one of them can be its `default_price`. Products and Prices are your store's catalog.
Every store has one, and test mode and live mode each keep their own.

The API serves create, retrieve, update, list, delete and search. The full list of parameters is in the
[Products API reference](https://docs.fluveo.com/api/products.md).

## Create a product with its first price

Send a `name`. Add `default_price_data` to create the first Price in the same request: the Product and the Price
are written together, and the new Price becomes the Product's `default_price`. An amount is a whole number of the
currency's smallest unit, so `unit_amount=2900` with `currency=usd` means $29.00.

**Create a product with its price**

cURL:

```bash
curl https://api.fluveo.com/v1/products \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: 4f1d2c7e-pro-tee" \
  -d name="Pro T-shirt" \
  -d description="Organic cotton, in five colors." \
  -d "default_price_data[currency]=usd" \
  -d "default_price_data[unit_amount]=2900"
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/products", {
  method: "POST",
  headers: { Authorization: "Bearer sk_test_...", "Idempotency-Key": "4f1d2c7e-pro-tee" },
  body: new URLSearchParams({
    name: "Pro T-shirt",
    description: "Organic cotton, in five colors.",
    "default_price_data[currency]": "usd",
    "default_price_data[unit_amount]": "2900",
  }),
});
const product = await response.json();
```

The answer is the Product, with `default_price` set to the new `price_...` id. Add `expand[]=default_price` to get
the whole Price in its place (see [Expanding responses](https://docs.fluveo.com/start/expand.md)). Without `default_price_data` the Product is
created with `default_price` null, and you add prices later with `POST /v1/prices`.

Other fields you can set on create and update:

- `description`, `url`, `metadata` (see [Metadata](https://docs.fluveo.com/start/metadata.md)). An empty `description` is refused on create
  and clears the text on update.
- `images`: up to 8 image URLs. Dashboards and checkouts draw the first one.
- `type`: `service` (the default) or `good`. `statement_descriptor` (up to 22 characters) and `unit_label`
  (up to 12) are taken only on a `service`; a `good` refuses both.
- `shippable`, `package_dimensions`, `marketing_features` (up to 15).
- `tax_code`: one of the product tax codes, such as `txcd_10000000` or `txcd_99999999`. Any other code is
  refused with `Invalid tax code`.
- `id`: send it to pick the Product's id yourself; leave it out and Fluveo makes a `prod_...` id. Your pick only has
  to be free in your own store and mode, and reusing one you already have answers `resource_already_exists`.

## Update a product

`POST /v1/products/{id}` changes only the fields you send. An empty value clears a field: `description=` sets it to
null, while leaving `description` out keeps it. `metadata` merges key by key, an empty value removes one key and
`metadata=` removes them all.

To change the default price, send `default_price` with the id of another Price of the same Product. A Price of
another Product, an archived Price and an unknown id are refused.

An update that changes nothing writes no event and keeps `updated` as it was.

## Archive or delete

Archiving is almost always what you want. Send `active=false`: the Product stays readable with all its Prices and
its history, it can still take new Prices, and `active=true` brings it back.

`DELETE /v1/products/{id}` removes a Product for good, and only one that has no Prices, active or archived. A Product
with any Price is refused with "This product cannot be deleted because it has one or more user-created prices.":
archive it instead. After a delete the id answers `resource_missing`, and a chosen id can be used again.

## List products

`GET /v1/products` returns your store's Products, newest first, paged with `limit` (1 to 100, default 10),
`starting_after` and `ending_before` (see [Pagination](https://docs.fluveo.com/start/pagination.md)). Filter with `active`, `created`,
`ids[]`, `shippable`, `type` and `url`. A `limit` outside 1 to 100 is refused, not clamped.

## Search products

`GET /v1/products/search` takes a `query` in the search query language and returns a `search_result`: `data`,
`has_more` and `next_page`. Pass `next_page` back as `page` to read on. It finds a write at once.

**Find active products whose name has "pro"**

cURL:

```bash
curl -G https://api.fluveo.com/v1/products/search \
  -H "Authorization: Bearer sk_test_..." \
  --data-urlencode 'query=active:"true" AND name~"pro"'
```

Node.js:

```javascript
const query = new URLSearchParams({ query: 'active:"true" AND name~"pro"' });
const response = await fetch("https://api.fluveo.com/v1/products/search?" + query, {
  headers: { Authorization: "Bearer sk_test_..." },
});
const result = await response.json();
```

The fields are `active`, `description`, `metadata`, `name`, `shippable` and `url`. `created` is not a search
field: a query on it is refused. More examples:

| Query | Finds |
| --- | --- |
| `name:"cotton shirt"` | names with the whole words "cotton shirt", in that order, any case |
| `name~"shirt"` | names that contain "shirt"; a `~` value needs 3 characters or more, a shorter one finds nothing |
| `metadata["sku"]:"PRO-1"` | products whose `sku` metadata is `PRO-1` |
| `-description:null` | products that have a description |
| `shippable:"true" OR name:"good"` | products that match either clause |

One query joins up to 10 clauses, all with `AND` or all with `OR`: a query that mixes the two is refused. So are
parentheses and the operators `>`, `<`, `>=`, `<=` and `=`, since no Product field is a number.
Add `expand[]=total_count` to get the number of matches.

## Events

A create sends `product.created`, an update `product.updated` with `previous_attributes`, and a delete
`product.deleted`. Each carries the whole Product. See [Webhooks](https://docs.fluveo.com/webhooks.md) and [Event types](https://docs.fluveo.com/webhooks/events.md).

> **For AI agents:**
>
> Archive a Product (active=false) instead of deleting it: a delete is refused while the Product has any Price.
> Create the first Price with default_price_data in the product create, so the Product never exists without it.
> Search fields for Products are active, description, metadata, name, shippable and url; created is not searchable.
