ghost-admin-api

Installation
SKILL.md

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.

Related skills
Installs
2
First Seen
Apr 11, 2026