Docs
API 2026-05-27.dahlia Dashboard

Catalog

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

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 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"
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). 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). 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). Filter with active, created, ids[], shippable, type and url. A limit outside 1 to 100 is refused, not clamped.

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 -G https://api.fluveo.com/v1/products/search \
  -H "Authorization: Bearer sk_test_..." \
  --data-urlencode 'query=active:"true" AND name~"pro"'
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:

QueryFinds
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:nullproducts 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 and Event types.