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.nameanddescription— seller-authored. The description is plain text and its newlines are significant; render with whitespace preserved.kind—digitalorphysical. 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, ornullfor 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 ofisSoldOut, which is a separate state deserving its own message.sortOrder— the product's place in this space's public list, counting from0, lowest first. It is dense: the list runs0, 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.type—one_time(a fixed price),pwyw(pay-what-you-want), orrecurring(billed on a cadence). An open enum; tolerate values you don't recognize.amountCents— integer cents, never a float and never a formatted string. Forone_timeit is the price; forpwywit is the minimum the buyer may pay (0means a free floor).suggestedCents— a suggested amount to prefill for apwywtier. Absent when there is no suggestion, and always absent on the other types.currency— a lowercase ISO 4217 code, e.g.usd.interval(monthoryear) andintervalCount— the billing cadence of arecurringtier. Both are absent, notnull, onone_timeandpwywtiers;intervalCountis always present alongsideinterval, and1means 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.