> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fourdos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Content API overview

> Read your club's published articles and render them on your own site, under your own domain and stylesheet

The content API is headless. Your club writes articles in the 4D console; your
website reads them over HTTP and renders them itself, on your own domain, with
your own stylesheet. 4D stores and renders the article body; nothing about how
it looks on your site is decided here.

Two endpoints, both read-only:

| Endpoint                          | Returns                                    |
| --------------------------------- | ------------------------------------------ |
| `GET /v1/content/articles`        | A page of published articles, newest first |
| `GET /v1/content/articles/{slug}` | One published article                      |

Both are documented in full on [Articles](/content/articles).

## Base URL

```
https://api.develop.fourdos.dev
```

Production base URLs are provided during onboarding.

## Before your first request

Two things have to be true, and both are done in the console rather than over
the API:

1. **The content module is installed** for your organization, on the
   integrations page. A club without it gets a `403` from both endpoints and
   loses no writing: uninstalling stops the API and deletes nothing.
2. **You hold a content key** with your site's origin on its allowlist. The
   key is publishable and the allowlist is what protects it. See
   [Authentication](/content/authentication).

Every request then has to carry an `Origin` header naming one of those
registered origins. Browsers set it for you. A server-side caller has to set it
itself, and a request without it is a `403` however good the key is, so this is
the first thing to check when a `curl` that looks right is refused.

## Two body formats

Every article is served either as rendered HTML or as a flat list of blocks.
The default is HTML.

* **`?format=html`** (the default) gives you one `html` string: the whole
  article body, already rendered and sanitised. Drop it into a container and
  style it with your own CSS. If you have no reason to prefer blocks, use this.
* **`?format=blocks`** gives you an ordered array of top-level blocks. Choose
  it when you want to render your own components for images and video rather
  than accepting our markup: for example, to route images through your site's
  responsive image component or your CDN, or to use your own video player. Text
  blocks still arrive as HTML fragments, so choosing blocks does not mean
  re-implementing rich text. See [Block format](/content/blocks).

Both formats come from one render performed when the article is published, so
the same article requested in both formats describes the same content, and a
read never re-renders anything.

One thing to know before you take the default: **a video embed is a `<div>` in
the HTML, not a player.** We serve the address on that element and never an
`<iframe>` from a third party. It renders as nothing until your site does
something with it, so an article carrying video wants blocks or wants your own
handling of that element. [Articles](/content/articles) has its shape.

## Drafts are unreachable

The read API serves published articles only. An unpublished draft is a `404`
even when its slug is guessed exactly, and it never appears in a list.

## Interaction events

Reporting what a reader did with an article is a separate, write-side surface:
`POST /v1/content-interactions`, one report per request. It reads best through
the [engagement SDK](/sdk/engagement-sdk#content-interactions), which is one call
from the page that just rendered the article:

```ts theme={null}
engage.reportInteraction({
  action: "content.read",        // or content.shared, content.liked
  id: article.id,
  userId: viewer.platformUserId
});
```

Three things about it are worth knowing before you wire it:

* **It authenticates with a different key.** Interactions ride the engagement
  rail and take a publishable tracking key (`ek_live_…`), not the content key
  these read endpoints take. One key per direction.
* **You do not send tags.** The server reads the article's tags off the article,
  so a read raises the member's affinity for exactly the tags the marketer chose.
* **Only signed-in readers count.** The report needs the reader's platform user
  id. An anonymous reader is invisible rather than counted anonymously, which is
  why these numbers sit below a club's own web analytics.

A read counts once per member, per article, per day, so a refresh does not
inflate it. [Render a club's articles](/walkthroughs/render-club-articles) puts
the call on a working page.

**Content interactions do not earn XP.** There is no path from reading an
article to a member's XP balance, and none is planned: a read reported by a
browser cannot be trusted to pay out. XP comes from
[engagement events](/concepts/events) under the rules your organization
configures.

## Where to go next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/content/authentication">
    Publishable keys, the origin allowlist, and what a rejected request gets back.
  </Card>

  <Card title="Articles" icon="newspaper" href="/content/articles">
    Both endpoints, every query parameter, and every response.
  </Card>

  <Card title="Block format" icon="cube" href="/content/blocks">
    The eight block types and the fields each one carries.
  </Card>

  <Card title="Render a club's articles" icon="code" href="/walkthroughs/render-club-articles">
    A working page that lists articles and renders one.
  </Card>

  <Card title="Report a read" icon="chart-line" href="/sdk/engagement-sdk#content-interactions">
    The one call that tells us a member read an article.
  </Card>
</CardGroup>
