Skip to main content

Products

A product is something a space sells. This API serves the public purchase page of a product — everything needed to render it and describe the offer, and nothing a buyer would only get after paying. Only products whose seller has published a public purchase page appear. Two endpoints cover them:

  • GET /v1/spaces/{space}/products — a space's public product index.
  • GET /v1/spaces/{space}/products/{slug} — one product's purchase page.

Both take any space identifier, and both return the same object. The index is not a narrower summary shape: a field can never be public in the list and withheld on the page, or the reverse.

This API is read-only — it cannot start a purchase. The buy flow lives on the space's own public site at {domain}.dfos.com/p/{slug}.

Listing a space's products

GET /v1/spaces/{space}/products is cursor-paginated (see Conventions), in the display order the space chose — the sortOrder field, lowest first. A space that has never arranged its store reads newest-first. It computes totalCount.

curl 'https://api.dfos.com/v1/spaces/field-notes/products?limit=20'

A product appears only when its seller has explicitly turned on its public page, it has a slug to be addressed by, and it is active and not archived. Nothing is public by inference, and turning a space's public profile off takes its whole index and every product page down with it.

An empty list is a valid answer, and it reveals nothing: a space that sells nothing, a space that has published none of what it sells, and a space that turned every page off all return the same empty page.

Fetching a product

GET /v1/spaces/{space}/products/{slug} fetches one purchase page by its slug — the /p/{slug} segment on the space's site:

curl 'https://api.dfos.com/v1/spaces/field-notes/products/softr-ticket'

The slug is a mutable alias chosen by the seller, and changing it breaks old links with no redirect. Store the id.

Product fields

  • id — the stable product identifier. Store this one.
  • slug — the mutable purchase-page alias.
  • name and description — seller-authored. The description is plain text and its newlines are significant; render with whitespace preserved.
  • kinddigital or physical. An open enum; treat an unrecognized value as an opaque string.
  • image — the hero image, when the product has one. Always a public, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL.
  • prices — every purchasable tier (see below).
  • purchaseGrantsMembership — whether buying makes the buyer a member of the space. This is a fact about the product, identical for every caller: it says what the purchase does, not what any particular person would get from it.
  • available — remaining stock, or null for unlimited stock. Advisory only — availability is re-checked when a purchase is actually made.
  • isSoldOut — true when limited stock is exhausted.
  • purchasable — whether a purchase can be started right now. It goes false when there is no tier a checkout could complete; render an unavailable state rather than a dead buy button. It is independent of isSoldOut, which is a separate state deserving its own message.
  • sortOrder — the product's place in this space's public list, counting from 0, lowest first. It is dense: the list runs 0, 1, 2, … with no gaps, because products the seller hasn't published are not counted at all. A position, not an identifier — publishing, unpublishing, or rearranging the store shifts it, so order by it and never store it as a key.

Price tiers

prices is an array — multiple tiers on one product are normal, for example a one-time price alongside a monthly subscription. Each tier carries:

  • id — the stable tier identifier. A product with more than one tier makes "buy this product" under-specified, so this is what names which tier a purchase is for.
  • typeone_time (a fixed price), pwyw (pay-what-you-want), or recurring (billed on a cadence). An open enum; tolerate values you don't recognize.
  • amountCentsinteger cents, never a float and never a formatted string. For one_time it is the price; for pwyw it is the minimum the buyer may pay (0 means a free floor).
  • suggestedCents — a suggested amount to prefill for a pwyw tier. Absent when there is no suggestion, and always absent on the other types.
  • currency — a lowercase ISO 4217 code, e.g. usd.
  • interval (month or year) and intervalCount — the billing cadence of a recurring tier. Both are absent, not null, on one_time and pwyw tiers; intervalCount is always present alongside interval, and 1 means every interval.

Format money from amountCents and currency yourself. A tier that a seller has deactivated is not listed at all.

What's not here

Nothing a buyer receives after paying reaches this API: downloadable files, confirmation instructions, and post-purchase links are the same exposure class as the paid goods themselves, and are never served here — pre-purchase or otherwise. Neither are seller and operations internals, nor anything viewer-relative: like every DFOS API response, a product is the same for every caller, so there is no "you already own this" signal.

When a product is not found

GET /v1/spaces/{space}/products/{slug} returns one identical 404 Not Found for every failure: the space has no public profile, the space does not exist, the slug matches nothing, the slug belongs to a different space, the seller never published a public page for it, or the product is archived or inactive. A caller cannot tell those apart — see Content visibility.

The index has only one not-found mode, the space itself, and it uses the same 404. Beyond that a list has nothing to not-find: it returns an empty page.