A headless CMS for content you didn't have to build
Read-only, publishable-key auth, edge-cached on Cloudflare. Fetch finished, quality-gated articles into any frontend — the same API this marketing blog runs on.
Base URL & authentication
Base: https://cdn.draftwave.io —
a dedicated worker, kept separate from the dashboard API so it can be cached hard and
scaled independently. Every /v1/*
route requires a key, sent as a bearer token or a query param:
Authorization: Bearer pk_live_xxx
# or, for simple frontends:
GET https://cdn.draftwave.io/v1/articles?site=SITE_ID&key=pk_live_xxx pk_live_… Publishable (read) key
Read-only, scoped to published content for one site.
Safe to ship in a browser — can be origin-restricted (a present, non-allowlisted
Origin header is
rejected with 403) and
is what powers content fetching.
sk_live_… Secret (admin) key Server-side only — broader access for scripting generation, drafts, and topics. Never expose it in a browser. Draftwave stores only a SHA-256 hash of every key and shows the full key exactly once, at creation.
Endpoints
Every endpoint only ever returns status = published
content scoped to the key's site — drafts and pending articles are never exposed, no
matter which key you use.
| Method | Endpoint | Query | Description |
|---|---|---|---|
| GET | /v1/articles | ?site=&limit=&cursor=&since=&tag= | List published articles for a site. Paginated (opaque cursor, limit capped at 100); since=<ms> powers incremental sync. |
| GET | /v1/articles/{slug} | ?site= | Fetch one published article by slug, with hreflang alternates for other published language variants. |
| GET | /v1/articles/{id}/raw | ?site= | The article body as Markdown — { markdown } — for consumers that don't want HTML. |
| GET | /v1/sites/{siteId}/manifest | ?since= | ids + updatedAt only — the cheapest way to know what changed for an incremental static build. |
| GET | /v1/sites/{siteId}/sitemap.xml | — | XML sitemap of published articles for the site's own domain. |
| GET | /v1/sites/{siteId}/feed.xml | — | RSS 2.0 feed of the 50 most recent published articles. |
| GET | /v1/sites/{siteId}/llms.txt | — | A curated, AI-crawler-friendly index of published content, grouped by topic cluster — 'robots.txt for the AI era.' |
The PublicArticle shape
Every article endpoint returns (or lists) the same object — rendered HTML, an optional Markdown body, FAQ pairs, and inject-ready JSON-LD, so you don't have to reassemble anything client-side.
interface PublicArticle {
id: string; slug: string;
title: string; metaTitle: string; metaDescription: string;
html: string; // rendered from Markdown server-side
markdown?: string; // only with ?include=markdown
excerpt: string;
faq: { q: string; a: string }[];
jsonLd: unknown; // ready to inject into <head>
hero: { url: string; alt: string } | null;
images: { url: string; alt: string }[];
cluster?: { id: string; title: string };
internalLinks: { slug: string; anchor: string }[];
tags: string[];
language: string;
alternates?: { language: string; slug: string }[];
publishedAt: number; updatedAt: number;
} Examples
curl — the 10 most recent published articles
curl "https://cdn.draftwave.io/v1/articles?site=SITE_ID&limit=10" \
-H "Authorization: Bearer pk_live_xxx" JS / Vue frontend (browser, origin-locked key)
const res = await fetch(
"https://cdn.draftwave.io/v1/articles?site=SITE_ID&limit=10",
{ headers: { Authorization: "Bearer pk_live_xxx" } }
);
const { data } = await res.json(); Incremental static build (server-side, in a build step)
const { items } = await fetch(
`https://cdn.draftwave.io/v1/sites/SITE_ID/manifest?since=${lastBuildMs}`,
{ headers: { Authorization: `Bearer ${process.env.DRAFTWAVE_KEY}` } }
).then((r) => r.json());
// fetch only the changed slugs, regenerate just those pages Caching & invalidation
Every response carries an ETag
and Cache-Control: public, max-age=60,
s-maxage=300, stale-while-revalidate=86400. Send
If-None-Match and get a
304 back for free. Publishing,
editing, or unpublishing an article bumps a per-site version counter that's folded into
every ETag, so caches invalidate correctly the moment content changes — no stale reads.
Rate limits
Each key is limited to 120 requests per 60-second window by default.
Go over it and you'll get a 429
with a Retry-After header
telling you exactly how long to back off.
Webhooks
For JAMstack and static-site consumers that want to rebuild the moment content
changes, register a URL (typically your Vercel/Cloudflare deploy hook) and Draftwave
POSTs on every event with an HMAC signature in
X-Draftwave-Signature for
you to verify:
site.article.published { siteId, articleId, slug, url }
site.article.updated
site.article.unpublished Content API FAQ
Do I need a secret key just to read published content?
No — a publishable key is all you need for every endpoint on this page. Secret keys are only for scripting write actions (triggering generation, reading drafts, managing topics) against the dashboard API, not the Content API.
What happens if I lose or leak a key?
Revoke it from Settings → API Keys and rotate — a revoked key returns 401 immediately. Because only a hash is stored, Draftwave can never show you the raw key again after creation, so treat the one-time reveal like a password.
Pull your first article via the API.
Start free, generate an article, and fetch it with a publishable key.