Skip to Content
REST APIProducts

Products

The catalogue, newest first.

stock: null means not counted

null is not zero. A product with trackInventory: false reports stock: null, which means Sailo is not counting units — not that there are none. A consumer syncing stock levels into a marketplace listing that reads one as the other will delist every product the seller sells without counting, which on a service or a digital download is most of them.

The field to gate a listing on is inStock, a boolean that already accounts for trackInventory. Use stock for a number to display, and only where trackInventory is true.

Variants are not expanded in the list

GET /products returns variants: [] on every row. GET /products/{id} returns them in full.

A page of twenty-five products with every variant inline is a large response nobody asked for, and the detail endpoint is one call away for the product that matters. If you are syncing a catalogue with variants, the pattern is: page the list, then fetch detail for the products you are actually mirroring.

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

kind decides the rest of the object

One of physical, digital, service, event, membership. It decides which of three optional objects is present:

kindCarries
servicebooking — duration, mode, lead time
eventevent — when it starts
membershipmembership — interval and trial days
physical, digitalnone of the three; all null

Filter on it when your integration only handles one:

curl "https://api.sailo.store/api/v1/products?kind=physical&published=true" \
  -H "Authorization: Bearer sailo_sk_…"

Drafts

?published=true is what buyers can see; ?published=false is drafts. Omitting it means both, which is deliberate — a storefront sync wants one, a catalogue audit wants the other, and neither should have to know which Sailo picked as a default.

Drafts are invisible on the storefront and readable through the API. That is the correct behaviour for a key the seller minted themselves, but it does mean a public mirror built from an unfiltered list will publish things the seller has not.

Prices are in the shop’s currency

Products carry no currency of their own — a shop prices in one. Every money object on every product carries the shop’s code, which you can read once from GET /shop.

compareAt is the struck-through “was” price where the seller set one, and null where they did not — not a copy of price.

The object

The full shape, including variants and the three optional objects, is on the product object reference.

GET /products

Variants are not expanded here — a page of twenty-five products with every variant inline is a large response nobody asked for, and the detail endpoint is one call away for the product that matters. stock is null on a product that does not track inventory, which means not counted and is not the same statement as sold out.

Parameters

Parameters for GET /products
ParameterWhat it does
kind query · stringWhat sort of thing it is. One of physical, digital, service, event, membership.
published query · booleantrue for what buyers can see, false for drafts. Omitting it means both — a storefront sync wants one, a catalogue audit wants the other, and neither should have to know which we picked as a default.
limit query · integerHow many to return. Defaults to 25, capped at 100 — asking for more is clamped, not refused.
cursor query · stringThe next_cursor from the previous page. Omit for the first page. A cursor we did not issue is a 400, not an empty page.

Request

curl "https://api.sailo.store/api/v1/products?published=true" \
  -H "Authorization: Bearer sailo_sk_…"

200 — a page of results

{
  "data": [
    {
      "id": "9a7e2c11-6b48-4d0f-8e35-71c9a4f2b6d8",
      "object": "product",
      "title": "Sourdough loaf",
      "slug": "sourdough-loaf",
      "kind": "physical",
      "price": { "cents": 4999, "amount": "49.99", "currency": "GBP" },
      "stock": 12,
      "inStock": true,
      "isPublished": true,
      "variants": []
      /* … every field GET /products/{id} returns, minus the variants … */
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Failures

Failure modes for GET /products
codeWhen it happens
invalid_request HTTP 400cursor is not one we issued.
unauthorized HTTP 401No Authorization header, or a key we do not recognise.
forbidden HTTP 403A real key, but the shop's plan does not include the API.
rate_limited HTTP 429Too many calls on this key. Slow down and retry.
server_error HTTP 500Our fault. The body says nothing about the cause; retry.

GET /products/{id}

The same product the list returns, plus its variants and their individual stock. A variant with no price of its own inherits the product's, so price is always populated.

Parameters

Parameters for GET /products/{id}
ParameterWhat it does
id path · string (uuid) · requiredThe product id.

Request

curl https://api.sailo.store/api/v1/products/9a7e2c11-6b48-4d0f-8e35-71c9a4f2b6d8 \
  -H "Authorization: Bearer sailo_sk_…"

200

{
  "data": {
    "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": "4c5d…",
        "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"
  }
}

Failures

Failure modes for GET /products/{id}
codeWhen it happens
not_found HTTP 404No product with that id in this shop.
unauthorized HTTP 401No Authorization header, or a key we do not recognise.
forbidden HTTP 403A real key, but the shop's plan does not include the API.
rate_limited HTTP 429Too many calls on this key. Slow down and retry.
server_error HTTP 500Our fault. The body says nothing about the cause; retry.
Last updated on