Skip to Content
ObjectsProduct

Product

What GET /products and GET /products/{id} return. No webhook carries a product — the catalogue is read, not pushed.

Fields on a product
FieldWhat it is
id stringThe product's id, a UUID.
object "product"Always the literal string.
title stringThe product's name.
slug stringIts URL segment on the storefront — sailo.store/{handle}/p/{slug}.
description string | nullThe seller's description, as plain text.
kind stringOne of physical, digital, service, event, membership. It decides what the rest of the object carries: only service has a booking, only event has an event, only membership has a membership.
tags string[]The seller's own labels. Normalised — lowercased and trimmed — before storage.
price moneyWhat it sells for, in the shop's currency.
compareAt money | nullThe struck-through 'was' price, where the seller set one. Null rather than equal to price when there is no comparison to draw.
trackInventory booleanWhether Sailo counts stock for this product at all.
stock number | nullUnits on hand, or null when trackInventory is false. Null means not counted, which is not the same statement as sold out — a consumer syncing levels into a marketplace listing must not read one as the other.
inStock booleanWhether it can be bought right now. Already accounts for trackInventory, so this — not stock — is the field to gate a listing on.
isPublished booleanWhether buyers can see it. A draft is invisible on the storefront but readable through the API.
isFeatured booleanWhether the seller pinned it to the top of their shop.
booking object | nullAppointment settings, on a bookable product. Null on everything else.
event object | null{ startsAt } on a ticketed event. Null on everything else.
membership object | nullRecurring billing settings, on a membership product. Null on everything else.
variants object[]Empty on GET /products — a page of twenty-five products with every variant inline is a large response nobody asked for. Populated on GET /products/{id}.
createdAt string | nullISO 8601.
updatedAt string | nullISO 8601.

Stock

stock: null means not counted, not sold out. A product with trackInventory: false reports null because Sailo is not tracking units — a service, a digital download, a made-to-order item. A consumer that reads null as zero will delist most of a typical shop.

Three fields, and only one of them is a listing gate:

FieldQuestion it answers
trackInventoryIs Sailo counting units at all?
stockHow many, or null if not counting.
inStockCan a buyer buy this right now?

inStock already accounts for trackInventory, so it is the field to gate on. Use stock only for a number you display, and only where trackInventory is true.

kind decides what else is present

Exactly one of the three optional objects, or none:

kindObject
servicebooking
eventevent
membershipmembership
physical, digitalall three null

booking

Fields on a product's booking object
FieldWhat it is
durationMinutes number | nullHow long one appointment runs.
serviceMode string | nullin_person or online.
leadHours number | nullHow far ahead a buyer must book. A same-day cutoff, expressed in hours.

event

Fields on a product's event object
FieldWhat it is
startsAt string | nullISO 8601, in UTC. Render it in the shop's timeZone.

membership

Fields on a product's membership object
FieldWhat it is
interval string | nullmonth or year.
trialDays number | nullFree days before the first charge. Null or zero means none.

A membership product is what a subscription is a subscription to — the subscription’s productId points here.

Variants

Fields on a product variant
FieldWhat it is
id stringThe variant's id — the value an order line's variantId carries.
sku string | nullThe seller's own code for this variant.
options objectThe choices that define it, as a plain object: { "Size": "Large", "Colour": "Blue" }. Keys are whatever the seller named their option groups.
price moneyAlways populated. A variant with no price of its own inherits the product's, so there is no null to handle.
stock number | nullUnits of this variant, or null when the product does not track inventory.
isAvailable booleanWhether this particular variant can be bought.

Empty on the list endpoint, populated on the detail endpoint. A page of twenty-five products with every variant inline is a large response nobody asked for. If you are mirroring a catalogue with variants, page the list and then fetch detail for the products you actually mirror.

options is a plain object whose keys are whatever the seller named their option groups — { "Size": "Large", "Colour": "Blue" }. Do not assume a fixed set of keys, and do not assume every variant of a product carries the same ones.

price on a variant is always populated: one with no price of its own inherits the product’s, so there is no null to handle.

slug and id

id is the key. slug is the product’s URL segment on the storefront — sailo.store/{handle}/p/{slug} — and a seller can change it. Same rule as the shop’s handle: store the id, treat the slug as display.

Drafts

isPublished: false is a draft: invisible on the storefront, readable through the API. Correct for a key the seller minted themselves, but it means an unfiltered mirror will publish things the seller has not. Filter with ?published=true unless you specifically want both.

Example

{ "id": "9a7e2c11-6b48-4d0f-8e35-71c9a4f2b6d8", "object": "product", "title": "Sourdough loaf", "slug": "sourdough-loaf", "description": "Baked the night before.", "kind": "physical", "tags": ["bread"], "price": { "cents": 4999, "amount": "49.99", "currency": "GBP" }, "compareAt": null, "trackInventory": true, "stock": 12, "inStock": true, "isPublished": true, "isFeatured": false, "booking": null, "event": null, "membership": null, "variants": [ { "id": "4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "sku": "SD-01-L", "options": { "Size": "Large" }, "price": { "cents": 5499, "amount": "54.99", "currency": "GBP" }, "stock": 4, "isAvailable": true } ], "createdAt": "2026-03-02T08:15:00.000Z", "updatedAt": "2026-08-01T16:20:31.442Z" }
Last updated on