Skip to main content

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​

FieldValue
idThe stable product identifier. Store this one.
slugThe mutable purchase-page alias.
name, descriptionSeller-authored. The description is plain text and its newlines are significant; render with whitespace preserved.
learnMoreUrlA 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.
kinddigital or physical. An open enum — treat an unrecognized value as an opaque string.
imageThe hero image, when the product has one. Always a public, permanently-hosted CDN image; this endpoint never emits a signed or expiring URL.
pricesEvery purchasable tier (see Price tiers).
purchaseGrantsMembershipWhether buying makes the buyer a member of the space. A fact about the product, identical for every caller.
availableRemaining stock, or null for unlimited. Advisory only — availability is re-checked when a purchase is made.
isSoldOutTrue when limited stock is exhausted.
purchasableWhether 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.
sortOrderThe 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.
ticketForOn 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).
createdAtWhen 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.

FieldValue
idThe stable tier identifier. It 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).
suggestedCentsA suggested amount to prefill for a pwyw tier. Absent when there is no suggestion, and always absent on the other types.
currencyA lowercase ISO 4217 code, e.g. usd.
interval, intervalCountThe 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.