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.
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 emptydescriptionis 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) orgood.statement_descriptor(up to 22 characters) andunit_label(up to 12) are taken only on aservice; agoodrefuses both.shippable,package_dimensions,marketing_features(up to 15).tax_code: one of the product tax codes, such astxcd_10000000ortxcd_99999999. Any other code is refused withInvalid tax code.id: send it to pick the Product's id yourself; leave it out and Fluveo makes aprod_...id. Your pick only has to be free in your own store and mode, and reusing one you already have answersresource_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.
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.
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:
| 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 and Event types.