ghost-admin-api
Ghost Admin API
Overview
The Ghost Admin API provides full read-write access to a Ghost publication. It handles content management (posts, pages, tags), member management, newsletter configuration, image/theme uploads, and webhook registration. Authentication uses JWT tokens generated from Admin API keys.
When to Use
- Creating, updating, publishing, scheduling, or deleting posts and pages
- Managing members (create, update, import, generate sign-in links)
- Configuring newsletters, tiers, and offers
- Uploading images, media, or themes
- Registering and managing webhooks
- Building publishing workflows, CMS integrations, or content pipelines
- Automating Ghost administration tasks
Authentication
Admin API Key Format
Admin API keys are obtained from Ghost Admin > Settings > Integrations > Custom Integration. They follow the format {id}:{secret} where the secret is a hex-encoded string.
JWT Token Generation
Generate a short-lived JWT (max 5 minutes) for each request:
JavaScript (Node.js):
const jwt = require('jsonwebtoken');
function generateToken(adminApiKey) {
const [id, secret] = adminApiKey.split(':');
const token = jwt.sign(
{},
Buffer.from(secret, 'hex'),
{
keyid: id,
algorithm: 'HS256',
expiresIn: '5m',
audience: '/admin/'
}
);
return token;
}
Python:
import jwt
from datetime import datetime as date
key = 'YOUR_ADMIN_API_KEY'
id, secret = key.split(':')
iat = int(date.now().timestamp())
header = {'alg': 'HS256', 'typ': 'JWT', 'kid': id}
payload = {'iat': iat, 'exp': iat + 5 * 60, 'aud': '/admin/'}
token = jwt.encode(payload, bytes.fromhex(secret), algorithm='HS256', headers=header)
Ruby:
require 'jwt'
key = 'YOUR_ADMIN_API_KEY'
id, secret = key.split(':')
iat = Time.now.to_i
header = {alg: 'HS256', typ: 'JWT', kid: id}
payload = { iat: iat, exp: iat + 5 * 60, aud: '/admin/' }
token = JWT.encode payload, [secret].pack('H*'), 'HS256', header
JWT Structure:
Header: {"alg": "HS256", "kid": "{id}", "typ": "JWT"}
Payload: {"iat": {timestamp_seconds}, "exp": {timestamp_seconds}, "aud": "/admin/"}
Critical: Timestamps must be Unix epoch seconds (not milliseconds). Max expiration is 5 minutes from iat.
Request Headers
Authorization: Ghost {token}
Accept-Version: v5.0
Content-Type: application/json
Base URL: https://{admin_domain}/ghost/api/admin/
Alternative: Staff Access Tokens
Users can generate personal tokens from Profile > Staff access token. These authenticate as the specific user with their role-based permissions.
JavaScript SDK (@tryghost/admin-api)
Installation
npm install @tryghost/admin-api
Initialization
import GhostAdminAPI from '@tryghost/admin-api';
const api = new GhostAdminAPI({
url: 'https://my-ghost-site.com',
key: '{id}:{secret}',
version: 'v5.0'
});
Important: The Admin API SDK is for server-side use only. Never expose Admin API keys in client-side code.
SDK Methods
// Posts - CRUD
const posts = await api.posts.browse({limit: 10, include: 'tags,authors'});
const post = await api.posts.read({id: '...'});
const newPost = await api.posts.add({title: 'My Post', lexical: '...'});
const updated = await api.posts.edit({id: '...', title: 'Updated', updated_at: '...'});
await api.posts.delete({id: '...'});
// Posts - Create from HTML
const htmlPost = await api.posts.add(
{title: 'From HTML', html: '<p>Content</p>'},
{source: 'html'} // Second param tells API to convert HTML to Lexical
);
// Pages - same interface as posts
const pages = await api.pages.browse({limit: 5});
const newPage = await api.pages.add({title: 'About'});
await api.pages.edit({id: '...', title: 'Updated About', updated_at: '...'});
await api.pages.delete({id: '...'});
// Images
const image = await api.images.upload({file: '/path/to/image.jpg'});
// Returns: [{url: 'https://...', ref: null}]
// Tags, Members, Newsletters, Tiers, Offers - similar patterns
Endpoints Reference
| Resource | Methods | Notes |
|---|---|---|
/posts/ |
Browse, Read, Add, Edit, Copy, Delete | Primary content resource |
/pages/ |
Browse, Read, Add, Edit, Copy, Delete | Static pages |
/tags/ |
Browse, Read, Add, Edit, Delete | Content taxonomy |
/members/ |
Browse, Read, Add, Edit | Subscription management |
/newsletters/ |
Browse, Read, Add, Edit | Email newsletters |
/tiers/ |
Browse, Read, Add, Edit | Membership tiers |
/offers/ |
Browse, Read, Add, Edit | Discount offers |
/users/ |
Browse, Read | Staff users (read-only for integrations) |
/images/ |
Upload | Image upload (multipart) |
/themes/ |
Upload, Activate | Theme management |
/site/ |
Read | Site information |
/webhooks/ |
Add, Edit, Delete | Event subscriptions (no Browse) |
Posts & Pages
Creating a Post
// Minimal post
const post = await api.posts.add({title: 'Hello World'});
// Full post with Lexical content
const post = await api.posts.add({
title: 'My Post',
lexical: JSON.stringify({
root: {
children: [{
children: [{detail: 0, format: 0, mode: "normal", style: "", text: "Hello world", type: "text", version: 1}],
direction: "ltr", format: "", indent: 0, type: "paragraph", version: 1
}],
direction: "ltr", format: "", indent: 0, type: "root", version: 1
}
}),
status: 'published',
tags: [{name: 'News'}, {slug: 'featured'}],
authors: [{email: 'author@site.com'}],
featured: true,
visibility: 'public',
feature_image: 'https://example.com/image.jpg',
custom_excerpt: 'A brief description',
meta_title: 'SEO Title',
meta_description: 'SEO Description',
canonical_url: 'https://original-source.com/post'
});
Creating from HTML Source
// Pass {source: 'html'} as second parameter
const post = await api.posts.add(
{
title: 'HTML Post',
html: '<h2>Heading</h2><p>Paragraph with <strong>bold</strong> text.</p>'
},
{source: 'html'}
);
Note: HTML-to-Lexical conversion is lossy. For lossless HTML, wrap content in card markers:
<!--kg-card-begin: html-->
<div>Your exact HTML preserved here</div>
<!--kg-card-end: html-->
Updating a Post
The updated_at field is required for edits (optimistic locking).
Critical: Tag and author relations are replaced, not merged on update. Always send the complete arrays.
const post = await api.posts.read({id: postId});
const updated = await api.posts.edit({
id: postId,
title: 'Updated Title',
tags: post.tags, // Must include ALL tags, not just new ones
updated_at: post.updated_at // Required for collision detection
});
Publishing & Scheduling
// Publish immediately
await api.posts.edit({id, status: 'published', updated_at});
// Schedule for future
await api.posts.edit({
id,
status: 'scheduled',
published_at: '2025-06-01T12:00:00.000Z',
updated_at
});
// Unpublish (revert to draft)
await api.posts.edit({id, status: 'draft', updated_at});
Email Distribution
// Publish and send as email to all members
await api.posts.edit({
id,
status: 'published',
email_segment: 'status:free,status:paid',
updated_at
});
// Email-only post (not published on site)
await api.posts.add({
title: 'Newsletter Only',
html: '<p>Email content</p>',
status: 'published',
email_only: true
}, {source: 'html'});
Visibility Settings
public - Visible to everyone
members - Visible to all members (free + paid)
paid - Visible to paid members only
tiers - Visible to specific tiers (set tiers array)
Content Formats
Ghost stores content in Lexical format (JSON). When reading, request additional formats:
?formats=html,lexical # Both formats
?formats=html,plaintext # HTML and plaintext
To create content from HTML, pass {source: 'html'} as the second parameter to add() or edit().
Members
Fetch Members
const members = await api.members.browse({
include: 'newsletters,labels',
filter: 'status:paid',
limit: 50
});
Create a Member
const member = await api.members.add({
email: 'user@example.com',
name: 'Jane Doe',
note: 'Signed up via API',
labels: [{name: 'VIP'}],
newsletters: [{id: 'newsletter-id'}]
});
Member Object Fields
id, uuid, email, name, note, geolocation, status (free/paid/comped), labels, subscriptions, newsletters, avatar_image, email_count, email_opened_count, email_open_rate, created_at, updated_at, last_seen_at
Subscription Object (Paid Members)
id, status, start_date, customer, default_payment_card_last4, cancel_at_period_end, cancellation_reason, current_period_end, price (amount, interval, currency), tier, offer
Newsletters
// List newsletters
const newsletters = await api.newsletters.browse();
// Create newsletter
const newsletter = await api.newsletters.add({
name: 'Weekly Digest',
description: 'A weekly roundup',
sender_name: 'My Publication',
sender_email: 'newsletter@site.com',
status: 'active',
subscribe_on_signup: true,
sort_order: 0
});
Tiers
// List tiers
const tiers = await api.tiers.browse();
// Create tier
const tier = await api.tiers.add({
name: 'Premium',
description: 'Full access',
monthly_price: 500, // in cents
yearly_price: 5000, // in cents
currency: 'usd',
benefits: ['Benefit 1', 'Benefit 2']
});
Offers
const offer = await api.offers.add({
name: 'Summer Sale',
code: 'SUMMER2024',
display_title: '50% Off',
display_description: 'Half price for the first year',
type: 'percent', // 'percent' or 'fixed'
amount: 50,
duration: 'once', // 'once', 'forever', 'repeating'
tier: {id: 'tier-id'}
});
Image Upload
// Upload from file path
const image = await api.images.upload({file: '/path/to/photo.jpg'});
console.log(image.url); // https://site.com/content/images/2024/01/photo.jpg
// Use in post
await api.posts.add({
title: 'Post with Image',
feature_image: image.url
});
cURL equivalent:
curl -X POST "https://site.com/ghost/api/admin/images/upload/" \
-H "Authorization: Ghost {token}" \
-F "file=@/path/to/image.jpg" \
-F "purpose=image"
Theme Management
# Upload theme
curl -X POST "https://site.com/ghost/api/admin/themes/upload/" \
-H "Authorization: Ghost {token}" \
-F "file=@/path/to/theme.zip"
# Activate theme
curl -X PUT "https://site.com/ghost/api/admin/themes/{theme-name}/activate/" \
-H "Authorization: Ghost {token}"
Query Parameters
| Parameter | Available On | Description |
|---|---|---|
include |
All | Embed related data |
fields |
All | Select response fields |
formats |
Posts, Pages | html, lexical, plaintext |
filter |
Browse | NQL filter expression |
limit |
Browse | Results per page (default 15, max 100) |
page |
Browse | Page number |
order |
Browse | Sort field and direction |
Error Handling
Ghost API errors return structured JSON:
{
"errors": [{
"message": "Human-readable message",
"context": "Additional context",
"type": "ValidationError",
"details": [],
"property": null,
"help": "Link to documentation",
"code": null,
"id": "unique-error-id"
}]
}
Common error types: ValidationError, UnauthorizedError, NotFoundError, VersionMismatchError
Breaking Changes (Ghost 6.0)
- Removed
?limit=all— max 100 results per page; paginate to fetch all - Requires Node.js v22+
- MySQL 8 only for production
- Session endpoint deprecated — use
/users/me/instead
Resources
references/admin_api_endpoints.md
Complete endpoint reference with all request/response schemas and field descriptions.
scripts/ghost-jwt.mjs
Standalone JWT token generator for non-JavaScript environments or testing.
More from bowtiedswan/ghost-cms-skills
ghost-cli
Ghost CLI tool for installing, configuring, and managing Ghost CMS instances. This skill should be used when installing Ghost, configuring a production server, managing Ghost processes (start/stop/restart), updating Ghost, setting up SSL/NGINX, backing up sites, or troubleshooting Ghost installations.
2ghost-webhooks
Ghost CMS webhooks and integrations for event-driven workflows. This skill should be used when setting up webhooks for Ghost events (post.published, member.added, etc.), building custom integrations, automating content workflows, or connecting Ghost to external services. Covers all 31 webhook events, payload handling, JavaScript SDKs, headless CMS setups, and migration tools.
2ghost-content-api
Read-only access to Ghost CMS published content via the Content API. This skill should be used when fetching posts, pages, tags, authors, tiers, or settings from a Ghost publication. Covers authentication with Content API keys, the JavaScript SDK (@tryghost/content-api), NQL filtering syntax, pagination, and all available endpoints.
2