# Content API

> Serve the posts Revnu writes on your own site and domain. The Content API returns published posts as JSON, read with an rvc_ token from Visibility → Publishing Settings; covers tokens, rotation, rate limits, the rebuild hook and backing up your posts if you leave.

Canonical: https://revnu.com/docs/content-api
Last updated: 2026-10-16

The Content API serves the posts Revnu writes and publishes as JSON, so your own site renders them with its own design, on its own domain and framework. Revnu writes, fact-checks and publishes through the customer's approval settings. Your site fetches the result. Nothing about your stack changes.

This is a separate surface from the [personal-token API](https://revnu.com/docs/developers.md): a different token, a different base URL, and read-only by construction.

## The contract

```
Base URL:  https://revnu.com/api/content/v1
Auth:      Authorization: Bearer rvc_...   (server-side env var only)

GET /posts                 all published posts, newest first, paged
GET /posts/{slug}          one post, render-ready
```

| Code | Meaning |
|---|---|
| 200 | ok |
| 400 | an unrecognised `type`, or a non-numeric `since` or `limit` on the list |
| 401 | missing or malformed `Authorization` header |
| 404 | unknown or revoked token, or unknown slug; indistinguishable on purpose |
| 429 | slow down; `Retry-After` is set |

Responses are never cached on Revnu's side. Your site's own build or revalidation is the cache.

Revnu counts your site as connected when it reads the first page of the full list: `GET /posts` with no `type` or `since` filter. A site that only ever sends filtered requests is never counted, so **I've Connected It** is refused and posts show as not on your site. Fetch the unfiltered list for your blog index.

Rate limit: about 300 requests a minute per token, with bursts of up to 600, so a full build that fetches the list and every post of a 200-post blog fits. Over the limit you get `429` with `Retry-After`; wait that many seconds and carry on. Page details: [List posts](https://revnu.com/docs/content-posts.md) and [Get a post](https://revnu.com/docs/content-post.md).

## Getting a token

The token is created by the blog setup, not by hand. It's the "API key" the setup screen shows, and it starts with `rvc_`:

1. In the dashboard, open **Visibility → Posts** and enter the blog's address.
2. Choose whether to import existing posts or start with new posts only.
3. The **Connect Your Site** screen shows the `rvc_` key once, with a setup prompt for you or your coding agent. Store the key in a server-side environment variable named `REVNU_CONTENT_API_KEY`.

Lost the key? You can't view it again. Rotate it in **Visibility → Publishing Settings → Content API** (next section) and use the new one.

After setup, the **Content API** card lives in **Visibility → Publishing Settings**. It shows the endpoints, when the token was minted, and the rebuild hook.

## Before you click I've Connected It

1. Deploy the integration with the current key.
2. Make sure your live site fetches the unfiltered post list on the server, for example by loading your blog index once.
3. Then click **I've Connected It** on the **Connect Your Site** screen.

Revnu opens your blog address once and waits a few seconds for your site to request posts. If it never does, the button is refused with "...hasn't requested any posts from Revnu yet". A request from your laptop with the key also counts, so test the deployed site, not a local copy. What the refusal means and how to clear it: [Why isn't my post live?](https://revnu.com/docs/blog-not-publishing.md)

## Rotating the token

In **Visibility → Publishing Settings → Content API**, click **Rotate Token**. The new token is shown once. The old one stops working immediately, so update your environment variable and redeploy straight away. Revisiting the setup screen never rotates the key.

## rvc_ and rvp_ tokens

| | `rvc_` content token | `rvp_` personal token |
|---|---|---|
| Created in | The blog setup; rotated in Publishing Settings | **Settings → Developers** |
| Base URL | `https://revnu.com/api/content/v1` | `https://revnu.com/api/v1` |
| Can do | Read posts already published on the blog | Act as you across Review, the agent, results, leads and workflows, within its scopes |
| If it leaks | Exposes only posts that are already public | Rotate it at once |

Never use an `rvp_` token to fetch posts, and never ship either token to the browser.

## The one hard rule

**Render server-side or at build time.** Next.js (SSG, ISR or SSR), Astro, Nuxt and SvelteKit all work. Fetching in the browser from a single-page app does not work for SEO: search and answer engines will not reliably see content that only exists after client-side JavaScript runs. For sites that can't render on the server, use the [Revnu-hosted blog](https://revnu.com/docs/hosted-blog.md). Worked examples: [Render posts in Next.js or Astro](https://revnu.com/docs/host-your-blog.md).

## Rebuild hook for static sites

A site that fetches posts only at build time shows a new post only after a rebuild. Create a deploy hook on your host and paste its URL under **Visibility → Publishing Settings → Content API → Rebuild Hook**. Revnu POSTs to it whenever a post is published, updated or removed:

```json
{ "event": "post.published", "blogSlug": "acme", "postSlug": "graph-rag", "updatedAt": 1753100000000 }
```

`event` is `post.published`, `post.updated` or `post.deleted`. The ping is unsigned by design and carries nothing worth trusting: treat it only as "rebuild now", then read the posts through the authenticated API. Revnu doesn't retry a failed ping. A site using ISR or SSR doesn't need the hook.

A rebuild also removes posts. A post you delete in Revnu drops out of the list, so a build that reads the list leaves out its page and its sitemap entry.

### No deploy hook on your host?

Some hosts (S3 and CloudFront, a self-built pipeline) have no deploy hook URL. Give Revnu a small endpoint of your own that starts your build instead. This one starts a GitHub Actions workflow through `workflow_dispatch`:

```ts
// A serverless function. Paste its URL, with ?key=..., as the Rebuild Hook.
export async function POST(req: Request) {
  // The ping is unsigned, so gate it with a secret of your own.
  if (new URL(req.url).searchParams.get("key") !== process.env.REBUILD_SECRET) {
    return new Response("forbidden", { status: 403 });
  }
  await fetch("https://api.github.com/repos/OWNER/REPO/actions/workflows/deploy.yml/dispatches", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
      Accept: "application/vnd.github+json",
    },
    body: JSON.stringify({ ref: "main" }),
  });
  return new Response("ok");
}
```

The workflow file needs `on: workflow_dispatch`, and the token needs permission to run Actions on that repo. Any other build trigger works the same way.

## If you leave

The token isn't tied to your plan. Today it keeps returning your published posts after you cancel or a payment fails, and it stops working only if your account is deleted or the token is rotated. That's current behaviour, not a guarantee, so take a copy while you're a customer.

This saves every published post, as markdown and HTML, into a `posts/` folder:

```bash
mkdir -p posts
cursor=""
while :; do
  page=$(curl -s -H "Authorization: Bearer $REVNU_CONTENT_API_KEY" \
    "https://revnu.com/api/content/v1/posts?limit=200${cursor:+&cursor=$cursor}")
  for slug in $(echo "$page" | jq -r '.posts[].slug'); do
    curl -s -H "Authorization: Bearer $REVNU_CONTENT_API_KEY" \
      "https://revnu.com/api/content/v1/posts/$slug" > "posts/$slug.json"
    sleep 0.2 # stays under the rate limit on large blogs
  done
  cursor=$(echo "$page" | jq -r '.cursor // empty | @uri')
  [ -z "$cursor" ] && break
done
```

Each file holds the post's `body.markdown`, `body.html`, metadata, FAQ and hero image URL. Hero images are hosted by Revnu, so download any you want to keep. Without a developer, email support@revnu.com for an export before you cancel. Your posts keep their paths, so you can republish them at the same URLs on your next setup. Deleting your account: [delete your account](https://revnu.com/docs/delete-account.md).

## Canonical URLs and the hosted copy

Once the blog's destination is your site, every `canonicalUrl`, `headMeta` and `jsonLd` value points at your domain, and the copy on the Revnu-hosted blog is kept out of search results so it can't compete with your pages.

## Search Console

Connect Google Search Console in the dashboard once your pages live on your domain. That is how Revnu sees, and reports, clicks, impressions and the queries you are winning. Without it the posts still ship, but reports can't include search performance. See [Connect Google Search Console](https://revnu.com/docs/search-console.md).

## Frequently asked questions

### Is this the same token as the personal API token?

No. Personal tokens start with `rvp_`, are minted in **Settings → Developers** and act as you across Review, the agent and results. Content tokens start with `rvc_`, come from the blog setup and can only read published posts.

### Where do I find my Content API key?

It's shown once, on the **Connect Your Site** screen of the blog setup in **Visibility → Posts**. If you've lost it, rotate it in **Visibility → Publishing Settings → Content API**. The old key stops working immediately.

### Which API key does my blog need?

The `rvc_` Content API key from the blog setup. A personal `rvp_` token from **Settings → Developers** is for the Revnu API, not your blog.

### Why does I've Connected It say my site hasn't requested any posts?

Your live site hasn't fetched the unfiltered post list with the current key. Deploy with the current key, load your blog index once, then click again.

### Can I go back to the Revnu-hosted blog later?

Yes. Email support@revnu.com and a person moves the blog's destination back to the hosted blog.

### Does the API send webhooks?

It sends the rebuild-hook ping, a hint to rebuild when posts change. The ping carries no content and isn't signed, so read the posts themselves through the API.

### Can my site save a copy of every post?

Yes. Page through `GET /posts` until `cursor` is null, then fetch each post. Each one includes both the markdown and the rendered HTML. [If you leave](#if-you-leave) has a script.

### Does my Content API key still work after I cancel?

Yes, as Revnu works today: the key doesn't check your plan, so your site keeps fetching published posts. It stops if your account is deleted or you rotate the key. Back up your posts before you leave.

### Where are the rate limits?

About 300 requests a minute per key, with bursts of up to 600. A `429` carries `Retry-After`.
