Choosing Between Ghost's Content API and Admin API for Headless Frontends
Ghost's Content API and Admin API serve fundamentally different roles. This guide maps each API to its intended workload—read-only public delivery vs. authenticated server-side mutations—with concrete JWT generation examples, rate-limit handling, and webhook verification.
26 Feb 2026, 05:07 UTC

The Decision: Which API Surface Fits Your Architecture
Ghost exposes two distinct API surfaces. The Content API serves read-only public data at high throughput. The Admin API provides full write access with stricter authentication and rate limits. Picking the wrong one forces workarounds—embedding secrets in browsers, hitting 429 errors during builds, or rebuilding authentication layers. This guide maps each API to the workload it was designed for.
Quick Comparison
| Dimension | Content API | Admin API |
|---|---|---|
| Access model | Read-only (GET, HEAD) | Full CRUD (POST, PUT, DELETE) |
| Authentication | Static key via key= query param |
Short-lived JWT (HS256, 5-min TTL) in Authorization header |
| Typical rate limit | ~1,000 req/min per IP/key | ~150 req/min per integration |
| CORS policy | Permissive (browser-friendly) | Restricted to configured integration domains |
| Scope of data | Posts, pages, tags, authors, tiers, settings (public) | Everything above + members, webhooks, integrations, settings (write), newsletters, offers |
| Version path (v4) | /ghost/api/content/v4/ |
/ghost/api/admin/v4/ |
| Safe for client-side | Yes | No—secret must stay server-side |
When to Use the Content API
Choose the Content API for any read path that runs in a browser, edge function, or public CDN. Examples:
- Rendering the public site (Next.js
getStaticProps, AstrogetStaticPaths, Remix loaders) - Search indexes (Algolia, Meilisearch, Typesense) built at build time or via scheduled jobs
- Syndication feeds (RSS, JSON Feed, newsletter digests)
- Public embed widgets ("latest posts" on a marketing site)
The static key is safe to expose. Rotate it quarterly via Settings → Integrations → Content API Key → Regenerate. No server-side token generation needed.
When to Use the Admin API
Reserve the Admin API for trusted server-to-server operations that mutate state:
- Content imports/migrations (markdown → Ghost, WordPress → Ghost)
- Member provisioning from external identity providers (Auth0, Clerk, Supabase)
- Webhook responders that create posts, update tiers, or send transactional emails
- Automated settings changes (toggle comments, update navigation, modify email templates)
- Custom admin tooling (internal dashboards, CLI utilities)
Never embed the Admin API secret in a SPA, mobile app, or client-side bundle. The secret signs JWTs; anyone with it gains full administrative control.
Concrete Implementation: Generating an Admin API JWT
Ghost expects a JWT signed with HS256 using the Admin API secret (base64url-encoded). The token expires in 5 minutes. Use the official @tryghost/admin-api SDK or implement manually.
Node.js Example (SDK)
// Run on your build server, CI runner, or backend service
// Requires: npm i @tryghost/admin-api
import GhostAdminAPI from '@tryghost/admin-api';
const api = new GhostAdminAPI({
url: 'https://your-site.ghost.io',
version: 'v4',
key: 'YOUR_ADMIN_API_SECRET' // format: "id:secret" from Integrations UI
});
// Create a post
const post = await api.posts.add({
title: 'Automated Import',
html: '<p>Content from external source</p>',
status: 'published'
});
console.log(post.id);
Where to run: Any Node.js 18+ environment with network access to your Ghost instance. Permissions: The integration must have "Posts" write scope (configured in Integrations → your integration → API Keys). Expected check: HTTP 201 with a post object containing id, uuid, status. Risk: If the secret leaks, rotate immediately in the Integrations UI.
Manual JWT Generation (No SDK)
// Run in any JS runtime (Node, Deno, Cloudflare Workers, Vercel Edge)
// Requires: Web Crypto API or 'jose' npm package
import { SignJWT } from 'jose';
const adminSecret = 'YOUR_ADMIN_API_SECRET'; // "id:secret"
const [id, secret] = adminSecret.split(':');
const key = await crypto.subtle.importKey(
'raw',
Uint8Array.from(atob(secret), c => c.charCodeAt(0)),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const token = await new SignJWT({})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT', kid: id })
.setIssuedAt()
.setExpirationTime('5m')
.setAudience('/admin/')
.sign(key);
// Use token in Authorization header
const res = await fetch('https://your-site.ghost.io/ghost/api/admin/v4/posts/', {
method: 'POST',
headers: {
'Authorization': `Ghost ${token}`,
'Content-Type': 'application/json',
'Accept-Version': 'v4'
},
body: JSON.stringify({ posts: [{ title: 'Test', html: '<p>Hi</p>', status: 'draft' }] })
});
console.log(res.status, await res.json());
Verification: Inspect response headers for X-RateLimit-Limit, X-RateLimit-Remaining. On 429, honor Retry-After (seconds).
Rate Limit Handling in Practice
Content API limits are generous for typical frontend traffic. Admin API limits require backoff logic for bulk operations.
async function adminRequestWithBackoff(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const retryAfter = Number(res.headers.get('Retry-After') || '5');
await new Promise(r => setTimeout(r, retryAfter * 1000));
}
throw new Error('Rate limit exceeded after retries');
}
Run this wrapper in your import scripts or webhook handlers. Log X-RateLimit-Remaining to monitor headroom.
CORS and Network Topology
Content API returns Access-Control-Allow-Origin: * by default—safe for direct browser calls. Admin API only allows origins listed in the integration's "Admin API Origin" field. For self-hosted Ghost behind a CDN/WAF (Cloudflare, AWS CloudFront, nginx):
- Allowlist
/ghost/api/admin/*paths for your integration server IPs - Or route Admin API traffic through a private network (VPC, Tailscale, Cloudflare Tunnel)
- Do not expose Admin API paths to the public internet without IP allowlisting
Webhook Verification
Ghost sends webhooks to Admin API endpoints. Verify the X-Ghost-Signature header using the integration secret:
// Express/Next.js API route example
import crypto from 'crypto';
export default async function handler(req, res) {
const signature = req.headers['x-ghost-signature'];
const hmac = crypto.createHmac('sha256', process.env.GHOST_ADMIN_SECRET.split(':')[1])
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== `sha256=${hmac}`) {
return res.status(401).send('Invalid signature');
}
// Process event
res.status(200).send('OK');
}
Without this check, anyone can spoof webhook payloads and trigger Admin API actions.
Versioning and Upgrade Safety
Both APIs use semantic versioning in the URL path (/v4/, /v5/). Breaking changes only arrive in major versions with a 12-month deprecation window. Before upgrading Ghost major versions:
- Check Ghost releases for API deprecation notices
- Test your integration against a staging instance on the new version
- Update the
Accept-Versionheader or SDK version pin
Limitations and Gotchas
- Content API cannot filter by custom fields—only built-in attributes (tags, authors, dates, status). For complex queries, fetch a superset and filter client-side or use a search index.
- Admin API does not support bulk delete—iterate with pagination and individual DELETE calls, respecting rate limits.
- Member API requires "Members" scope on the integration; default integrations may not have it enabled.
- JWT clock skew: Ensure your server time is synchronized (NTP). A 30-second skew causes 401 errors.
Validation Checklist
After wiring an integration, run these checks:
- Content API:
curl "https://your-site.ghost.io/ghost/api/content/v4/posts/?key=YOUR_CONTENT_KEY&limit=1"→ 200 withpostsarray. - Admin API: Generate a JWT, then
curl -H "Authorization: Ghost $JWT" -H "Accept-Version: v4" https://your-site.ghost.io/ghost/api/admin/v4/posts/→ 200 withpostsarray (or 401/403 if scopes missing). - Rate limits: Inspect
X-RateLimit-LimitandX-RateLimit-Remainingheaders on both endpoints. - Webhook: Use Ghost's "Test webhook" button in Integrations UI; verify your endpoint returns 200 and logs the payload.
Decision Summary
Use Content API for every public read path—site rendering, search, feeds, widgets. Use Admin API exclusively on trusted servers for writes, member management, and automation. Keep the Admin secret off client bundles. Rotate Content keys periodically. Monitor rate-limit headers. Verify webhook signatures. That's the full contract.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.