Open App

Skribra API

Fetch your published articles from Skribra and render them on your own site. One API key, three endpoints, no infrastructure of your own.

Overview#

Skribra has two ways to get articles onto a site you control, and they are opposites.

The webhook pushes. Skribra POSTs each article to an endpoint you host, once, at the moment it publishes. You have to receive it, store it, and serve your blog from your copy — which means running an endpoint and a database, and keeping that copy correct as articles get refreshed and rewritten.

The API pulls. You ask for articles when you render. There is nothing to receive and nothing to store, because Skribra stays the source of truth. If you would rather not run a database to hold a copy of your own blog, this is the one you want.

Both are available at once, and they return the same article shape, so moving between them does not mean rewriting your templates.

Authentication#

Every request carries a bearer token. Generate a key under Integrations → Skribra API in your dashboard.

Authorization: Bearer skr_live_9fK2xQ7vN4pL8sT1wY6zR3bM5cJ0hG2d

The key is account-level: one key reaches every project you enable, and rotating it breaks anything still using the old one, everywhere. If Authorization is awkward in your runtime, send X-Api-Key instead.

Server-side only. This key reads every enabled project on your account, so it must never reach a browser. Keep it in an environment variable and call the API from your server, your build, or a serverless function.

Enabling a project#

A key on its own fetches nothing. Each project has its own switch under Integrations → Skribra API, off by default, so a key can never expose a project you did not mean to share. Requests for a project that is off return 403 project_api_disabled.

Endpoints#

Base URL: https://api.skribra.com/v1

EndpointReturns
GET /v1/projectsYour projects, and whether the API is on for each
GET /v1/articlesA page of published articles, without the body
GET /v1/articles/:slugOne article, with its full HTML

List projects#

Start here — this is how you find the projectId every other call needs. Projects with the API switched off are listed too, with api_enabled: false, so a 403 elsewhere is easy to explain.

List articles#

curl "https://api.skribra.com/v1/articles?projectId=65f0…010&page=1&limit=12" \
  -H "Authorization: Bearer $SKRIBRA_API_KEY"
ParameterTypeRequiredDescription
projectIdstringYesWhich project to read. Get it from GET /v1/projects.
pagenumberNo1-based. Defaults to 1.
limitnumberNoDefaults to 20, capped at 100.
updatedSinceISO 8601NoOnly articles created or edited since this moment.
sortstringNopublished_at (default), updated_at, or title.
orderstringNodesc (default) or asc.

List rows are deliberately light — title, slug, excerpt, cover image, dates. The article body is the bulk of an article and an index page does not need it, so it is not here.

{
  "data": {
    "items": [
      {
        "id": "65f0…01a",
        "title": "How to measure activation rate",
        "slug": "how-to-measure-activation-rate",
        "excerpt": "Activation is the moment a new user…",
        "image_url": "https://cdn.skribra.com/…jpg",
        "type": "Guide",
        "reading_time": 7,
        "published_at": "2026-08-14T00:00:00.000Z",
        "updated_at": "2026-08-19T09:12:44.000Z"
      }
    ],
    "pagination": { "page": 1, "limit": 12, "total": 84, "total_pages": 7, "has_more": true }
  }
}

Unpublished articles, and any that Curator has redirected away during consolidation, are filtered out server-side. What you get back is what should be on your blog.

Get an article#

GET /v1/articles/:slug?projectId=… returns the full article. content_html is the body on its own — no table of contents, no related-articles block, no JSON-LD baked in. Those come back as toc_html, related_articles and json_ld beside it, so you can place each one where your layout wants it rather than unpicking one blob.

Slugs are unique within a project, not across your account, so projectId is required here too.

Rendering a blog#

The whole integration, with no database anywhere in it:

// app/blog/[slug]/page.tsx — the whole integration, no database.
const API = "https://api.skribra.com/v1";
const headers = { Authorization: `Bearer ${process.env.SKRIBRA_API_KEY}` };
const PROJECT = process.env.SKRIBRA_PROJECT_ID;

// Rebuild every hour so Keeper's refreshes and Curator's rewrites land.
export const revalidate = 3600;

export async function generateStaticParams() {
  const res = await fetch(`${API}/articles?projectId=${PROJECT}&limit=100`, { headers });
  const { data } = await res.json();
  return data.items.map((a) => ({ slug: a.slug }));
}

export default async function Page({ params }: { params: { slug: string } }) {
  const res = await fetch(
    `${API}/articles/${params.slug}?projectId=${PROJECT}`,
    { headers }
  );
  if (!res.ok) notFound();

  const { data: article } = await res.json();

  return (
    <article>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.content_html }} />
    </article>
  );
}

The index page is the same shape: fetch /v1/articles, map over data.items, link each card at its slug. Use data.pagination.total_pages to render your pager.

Caching and staying in sync#

Articles do not stop changing once they publish. Keeper refreshes them as they age and Curator rewrites or consolidates them, so a copy you took last month is already out of date. Two ways to keep up:

Revalidate on a timer. The simplest thing that works. Rebuild or revalidate hourly and you are never far behind. Responses carry Cache-Control: public, max-age=60 and an ETag, so a conditional request costs almost nothing.

Or sync incrementally. Pass updatedSince with the timestamp of your last run and you get only what changed. Useful when you have hundreds of articles and re-walking every page is wasteful.

Errors and rate limits#

Errors return a stable code alongside the message, so you can branch on the code and show the message.

// 403 — the key is valid, but this project has the API switched off.
{
  "code": "project_api_disabled",
  "message": "API access is not enabled for this project"
}
StatusCodeMeaning
400invalid_queryA parameter is missing or malformed — the message names which.
401unauthorizedMissing, unknown, or revoked API key.
403project_api_disabledYour project, but the API is switched off for it.
404project_not_foundNo such project on this account.
404not_foundNo published article with that slug in that project.
429rate_limitedOver the limit. Honour the Retry-After header.

Limits are per account: 300 list requests an hour and 2,000 a day, with more headroom on the article endpoint. Those are sized for building and revalidating, not for calling Skribra on every page view — cache responses on your side and you will not come near them.

FAQ#

Do I need a database? No. That is the point of this over the webhook. Fetch when you render, or at build time, and Skribra remains the only copy.

Can I use both the API and the webhook? Yes. They return the same article shape, so you can push into a cache and pull for anything the push missed.

Does one key read several projects? Yes — every project you switch on. Pass a different projectId per call.

What happens when I unpublish an article? It disappears from the list and its slug starts returning 404. If you cache, it clears on your next revalidation.

Was this page helpful?

skribra

AI-powered SEO content automation for blogs and websites.

© Copyright 2026. All rights reserved.