Products
A product is something a space sells. This API serves the public purchase page: 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.
GET /v1/spaces/{space}/products— a space's public product index.GET /v1/spaces/{space}/products/{slug}— one product's purchase page.GET /v1/products— public products across every discoverable space.
The two space-scoped endpoints take any
space identifier. All three return the same
object; the index is not a narrower summary shape. The cross-space feed adds one
thing, the space an item belongs to.
There is no purchase flow on this API — 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/home/products?limit=20'
A product appears only when its seller has turned on its public page, it has a slug to be addressed by, and it is active and not archived. Turning a space's public profile off takes its whole index and every product page down with it.
A seller can also unlist a product. An unlisted product still resolves at
/products/{slug}, but it is absent from the space's index and from the
cross-space feed.
An empty list is a valid answer: a space that sells nothing, one that has published none of what it sells, and one that turned every page off all return the same empty page.
Listing products across spaces
GET /v1/products returns every product with a public purchase page across
every publicly-discoverable space, newest-created first, cursor-paginated.
curl 'https://api.dfos.com/v1/products?limit=20'
Each item is the same product object plus the space it belongs to — that
space's id, did, domain, displayName, and avatarUrl. Pagination is the
whole input surface: no filters, not category, not price, not kind.
The space gate here is public discovery, the same predicate as
GET /v1/spaces, rather than direct lookup. A
space that is directly addressable can still be absent from this feed, and a
space turning its public site off drops every one of its products out of it.
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/home/products/the-dark-forest-anthology-of-the-internet-digital'
The slug is a mutable alias chosen by the seller, and changing it breaks old
links with no redirect. Store the id.
Product fields
| Field | Value |
|---|---|
id | The stable product identifier. Store this one. |
slug | The mutable purchase-page alias. |
name, description | Seller-authored. The description is plain text and its newlines are significant; render with whitespace preserved. |
learnMoreUrl | A seller-authored link to more information about the product, shown to buyers before purchase. Treat it as an untrusted external URL. Absent, not null, when the seller set none. |
kind | digital 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 Price tiers). |
purchaseGrantsMembership | Whether buying makes the buyer a member of the space. A fact about the product, identical for every caller. |
available | Remaining stock, or null for unlimited. Advisory only — availability is re-checked when a purchase is 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. Independent of isSoldOut. |
sortOrder | The product's place in this space's public list, counting from 0, lowest first, dense with no gaps. A position, not an identifier — order by it, never store it as a key. |
ticketFor | On a ticket product, the next upcoming public event occurrence it admits: seriesId, recurrenceId, and that occurrence's startsAt, or its startDate for an all-day event (see Events). |
createdAt | When the product was created (ISO 8601 UTC) — not when it was published. The cross-space feed is ordered by it. |
sortOrder shifts when the seller publishes, unpublishes, or rearranges the
store; unpublished products are not counted at all. ticketFor is singular — a
product may admit several events, and it names the soonest upcoming occurrence
across all of them. It is absent, not null, when the product is not a ticket,
when its ticket binding was detached, when the event it admits is not publicly
visible, and when no occurrence falls inside the window the events endpoints
look ahead; those causes are indistinguishable.
Price tiers
prices is an array; multiple tiers on one product are normal, for example a
one-time price alongside a monthly subscription. A tier a seller has deactivated
is not listed at all. Format money from amountCents and currency yourself.
| Field | Value |
|---|---|
id | The stable tier identifier. It names which tier a purchase is for. |
type | one_time (a fixed price), pwyw (pay-what-you-want), or recurring (billed on a cadence). An open enum — tolerate values you don't recognize. |
amountCents | Integer 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, intervalCount | The billing cadence of a recurring tier: month or year, and how many. Both absent, not null, on one_time and pwyw tiers; intervalCount is always present alongside interval, and 1 means every interval. |
What's not here
Nothing a buyer receives after paying reaches this API: downloadable files, confirmation instructions, and post-purchase links are never served here. Neither are seller and operations internals, nor anything viewer-relative — 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.
The index has one not-found mode, the space itself, with the same 404; beyond
that it returns an empty page. See
Content visibility.