Skip to main content

Show your space's posts on your own site

A common use of the DFOS API: render your public space's posts on a site you control — a marketing page, a blog, a landing page. This recipe walks the whole path. It assumes your space has a public profile turned on.

1. Fetch the space (optional)

If you want your space's name, avatar, or links in the header, fetch the space once:

curl https://api.dfos.com/v1/spaces/field-notes

Address it by subdomain, entity id, or protocol DID. Store the id or did, not the subdomain — the subdomain is a mutable alias. See Spaces.

2. List the public posts

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

Posts come back newest first. Each item gives you a displayTitle (already handles untitled posts), an excerpt, a cover, counts, isPinned, and publishedAt. If you want pinned posts at the top, sort on isPinned yourself — the feed does not reorder them. Page with nextCursor (see Conventions).

3. Link each post — or fetch its body

The listing does not include post bodies. You have two options:

  • Link out. Fetch each post and link to its canonicalUri — the canonical public permalink, which respects the space's custom domain. Cheapest path; DFOS renders the post.
  • Render inline. Fetch the single post and render its body yourself when it is readable.

Fetch a single post by its id (not its slug):

curl https://api.dfos.com/v1/spaces/field-notes/posts/post_4k8m2q7v

The response is a union on state. Handle both arms:

async function renderPost(space, postId) {
const res = await fetch(`https://api.dfos.com/v1/spaces/${space}/posts/${postId}`);
if (res.status === 404) return null; // missing, private, or wrong space
const data = await res.json();

if (data.state === 'eligible') {
const post = data.post;
// post.folded === true means only the above-fold teaser is present;
// link to post.canonicalUri for the rest.
return { title: post.displayTitle, body: post.body, href: post.canonicalUri };
}

// data.state === 'gated' — no body; show a join / subscribe CTA.
// Treat an unrecognized data.reason as 'restricted'.
return { title: data.space.displayName, cta: data.reason, join: data.space.domain };
}

Two things to respect:

  • The gated arm carries no content — render the space CTA (its domain, joinMode, and subscribeEnabled tell you how to prompt a join or subscribe), never a fake preview.
  • folded === true means the body is truncated at a fold. Show the teaser and link to canonicalUri; there is no anonymous way to fetch below-fold content.

4. Cache and stay compatible

  • Fetch fresh; don't blind-cache. Data responses are served Cache-Control: no-store — the API does not want you sharing a cache of them, because a post response can carry inline, time-limited signed media URLs. Fetch a resource when you need it, and watch the RateLimit-* headers and the 429 retry signal so you stay inside your budget. If you proxy the API from your own server, pass the responses through — do not introduce your own shared cache of data responses. See Rate limits and caching.
  • Tolerate growth. Ignore fields you don't recognize, and treat every enum (joinMode, siteMode, the gated reason) as open — new values can appear without a version bump. See the compatibility contract.
  • Store stable ids. Key your own storage on id / did, never on subdomains, handles, or slugs.

See Posts and Spaces for the full field semantics, and the interactive reference to try every endpoint.