Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Ghost

To integrate Replit with Ghost, generate a Content API key for public content and an Admin API key for write access from your Ghost Admin settings, store them in Replit Secrets (lock icon πŸ”’), and call the Ghost Content or Admin API from Python or Node.js. Ghost is a modern headless blog CMS with clean REST and JSON Content APIs suited for building custom frontends on Replit.

What you'll learn

  • How to generate Ghost Content API and Admin API keys
  • How to store Ghost credentials in Replit Secrets and build a secure backend
  • How to fetch posts, pages, and tags from Ghost using Python and Node.js
  • How to create and update Ghost posts programmatically via the Admin API
  • How to build a headless Ghost frontend served from Replit
Book a free consultation
4.9Clutch rating ⭐
600+Happy partners
17+Countries served
190+Team members
Intermediate13 min read25 minutesCMSMarch 2026RapidDev Engineering Team
TL;DR

To integrate Replit with Ghost, generate a Content API key for public content and an Admin API key for write access from your Ghost Admin settings, store them in Replit Secrets (lock icon πŸ”’), and call the Ghost Content or Admin API from Python or Node.js. Ghost is a modern headless blog CMS with clean REST and JSON Content APIs suited for building custom frontends on Replit.

Why Connect Replit to Ghost?

Ghost is one of the cleanest headless CMS options available β€” it delivers well-structured JSON for posts, pages, tags, authors, and newsletters without the configuration overhead of WordPress. Many technical founders use Ghost for their blog or documentation, then want to build a custom frontend, embed content in their app, or automate publishing workflows from code. Replit is an excellent platform for both the backend API layer and for running automation scripts that interact with Ghost.

The Ghost Content API is designed for public consumption β€” you use a key as a query parameter to fetch published content, and it is safe to call from browser clients. The Ghost Admin API is the power user interface: it can create drafts, publish posts, manage members, upload images, and send newsletters. The Admin API requires a signed JWT token generated from your Admin API key, making it server-only by design β€” a perfect match for Replit's backend code.

Common Replit + Ghost integration patterns include building a custom blog frontend that fetches Ghost posts and renders them with a custom design, automating cross-posting from Ghost to social media triggered by new post webhooks, batch-importing posts from another platform during a Ghost migration, and building a Members API dashboard showing subscriber analytics.

Integration method

Standard API Integration

You connect Replit to Ghost by creating API keys in your Ghost Admin panel and storing them in Replit Secrets. The Content API (read-only, safe for public clients) uses a key as a query parameter. The Admin API (full read/write access) requires a JWT token generated from your Admin API key and should only be called from your server-side Replit backend. Both APIs return clean JSON with predictable schemas.

Prerequisites

  • A Replit account with a Python or Node.js project created
  • A Ghost installation (Ghost Pro hosted account at ghost.org, or self-hosted Ghost instance)
  • Access to Ghost Admin panel to generate API keys
  • Basic familiarity with REST APIs and JSON
  • Node.js 18+ or Python 3.10+ (both available on Replit by default)

Step-by-step guide

1

Generate Ghost API Keys

Log in to your Ghost Admin panel (typically at yourdomain.com/ghost) and navigate to Settings in the left sidebar. Scroll down and click on 'Integrations'. Click 'Add custom integration' and give it a name like 'Replit Integration'. After creating the integration, you will see two API keys: The Content API Key is a long string used as a query parameter for public read-only requests. It is safe to use in client-side code and does not require authentication headers. The Admin API Key is in the format ID:SECRET (with a colon separating two components). This key is used to generate JWT tokens for Admin API requests. The Admin API key grants full write access to your Ghost installation β€” treat it like a database password. Also note your Ghost API URL β€” this is your Ghost site URL (e.g., https://yourblog.ghost.io or https://yourdomain.com). All API endpoints are constructed as GHOST_URL/ghost/api/content/... and GHOST_URL/ghost/api/admin/... Copy both keys and your Ghost URL before leaving the Integrations page.

Pro tip: Ghost Cloud (ghost.org) and self-hosted Ghost instances both use the same API. The only difference is your base URL β€” ghost.io domains for hosted accounts and your custom domain for self-hosted.

Expected result: You have a Content API key, an Admin API key (in ID:SECRET format), and your Ghost URL ready to add to Replit Secrets.

2

Store Ghost Credentials in Replit Secrets

Open your Replit project and click the lock icon πŸ”’ in the left sidebar. Add the following secrets: - Key: GHOST_URL β€” Value: your Ghost site URL (e.g., https://yourblog.ghost.io), no trailing slash - Key: GHOST_CONTENT_API_KEY β€” Value: your Content API key - Key: GHOST_ADMIN_API_KEY β€” Value: your Admin API key in full ID:SECRET format In Python, access these with os.environ['GHOST_URL'] etc. In Node.js, use process.env.GHOST_URL. The Content API key is relatively low risk (read-only, public content), but the Admin API key provides full write access including creating posts, managing members, and sending newsletters β€” store it in Replit Secrets and never expose it to client-side code or commit it to Git. Replit's Secret Scanner monitors for accidentally committed API keys.

Pro tip: Ghost API keys are specific to the Ghost installation, not to individual user accounts. If you delete and recreate the custom integration, you will get new keys and need to update Replit Secrets.

Expected result: GHOST_URL, GHOST_CONTENT_API_KEY, and GHOST_ADMIN_API_KEY appear in Replit Secrets with masked values.

3

Fetch Content Using Python

The Ghost Content API is the simplest API β€” include your content key as the key query parameter and you receive clean JSON. The API supports filtering, pagination, sorting, and field selection. Endpoints include /posts/, /pages/, /tags/, /authors/, and /tiers/. The Admin API requires a JSON Web Token (JWT) generated from your Admin API key. Split the key on ':' to get the key ID and secret, then sign a JWT payload with a 5-minute expiry. The Ghost Admin API documentation shows the exact JWT structure required. The code below implements a complete Ghost client for both the Content and Admin APIs, including JWT token generation for Admin calls.

ghost_client.py
1import os
2import time
3import hmac
4import hashlib
5import struct
6import base64
7import json
8import requests
9from typing import Optional
10
11GHOST_URL = os.environ["GHOST_URL"].rstrip("/")
12CONTENT_KEY = os.environ["GHOST_CONTENT_API_KEY"]
13ADMIN_API_KEY = os.environ["GHOST_ADMIN_API_KEY"]
14
15# ---- Content API (public, read-only) ----
16
17def get_posts(limit: int = 10, include: str = "tags,authors", filter: str = "") -> list:
18 """Fetch published posts from Ghost Content API."""
19 params = {
20 "key": CONTENT_KEY,
21 "limit": limit,
22 "include": include,
23 "order": "published_at desc"
24 }
25 if filter:
26 params["filter"] = filter
27 response = requests.get(f"{GHOST_URL}/ghost/api/content/posts/", params=params)
28 response.raise_for_status()
29 return response.json().get("posts", [])
30
31def get_post_by_slug(slug: str) -> Optional[dict]:
32 """Fetch a single post by its slug."""
33 params = {"key": CONTENT_KEY, "include": "tags,authors"}
34 response = requests.get(f"{GHOST_URL}/ghost/api/content/posts/slug/{slug}/", params=params)
35 if response.status_code == 404:
36 return None
37 response.raise_for_status()
38 posts = response.json().get("posts", [])
39 return posts[0] if posts else None
40
41def get_tags() -> list:
42 """Fetch all tags from Ghost."""
43 params = {"key": CONTENT_KEY, "limit": "all"}
44 response = requests.get(f"{GHOST_URL}/ghost/api/content/tags/", params=params)
45 response.raise_for_status()
46 return response.json().get("tags", [])
47
48# ---- Admin API (requires JWT) ----
49
50def _generate_admin_jwt() -> str:
51 """Generate a Ghost Admin API JWT token (valid for 5 minutes)."""
52 key_id, secret = ADMIN_API_KEY.split(":")
53 header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256", "kid": key_id, "typ": "JWT"}).encode()).rstrip(b"=").decode()
54 iat = int(time.time())
55 payload = base64.urlsafe_b64encode(json.dumps({"iat": iat, "exp": iat + 300, "aud": "/admin/"}).encode()).rstrip(b"=").decode()
56 signature_input = f"{header}.{payload}".encode()
57 secret_bytes = bytes.fromhex(secret)
58 sig = hmac.new(secret_bytes, signature_input, hashlib.sha256).digest()
59 signature = base64.urlsafe_b64encode(sig).rstrip(b"=").decode()
60 return f"{header}.{payload}.{signature}"
61
62def _admin_headers() -> dict:
63 return {"Authorization": f"Ghost {_generate_admin_jwt()}", "Content-Type": "application/json"}
64
65def create_post(title: str, html: str, status: str = "draft", tags: list = None) -> dict:
66 """Create a post via the Ghost Admin API."""
67 body = {
68 "posts": [{
69 "title": title,
70 "html": html,
71 "status": status, # 'draft' or 'published'
72 "tags": [{"name": t} for t in (tags or [])]
73 }]
74 }
75 response = requests.post(
76 f"{GHOST_URL}/ghost/api/admin/posts/",
77 headers=_admin_headers(),
78 json=body
79 )
80 response.raise_for_status()
81 return response.json().get("posts", [{}])[0]
82
83def update_post(post_id: str, updated_at: str, **fields) -> dict:
84 """Update a Ghost post. updated_at is required for conflict detection."""
85 body = {"posts": [{"updated_at": updated_at, **fields}]}
86 response = requests.put(
87 f"{GHOST_URL}/ghost/api/admin/posts/{post_id}/",
88 headers=_admin_headers(),
89 json=body
90 )
91 response.raise_for_status()
92 return response.json().get("posts", [{}])[0]
93
94if __name__ == "__main__":
95 posts = get_posts(limit=5)
96 print(f"Latest {len(posts)} posts:")
97 for p in posts:
98 print(f" {p['title']} ({p['slug']}) β€” {p['published_at']}")
99
100 tags = get_tags()
101 print(f"\nTags: {[t['name'] for t in tags]}")

Pro tip: When creating a post via the Admin API, you can use either html (standard HTML) or lexical (Ghost's native editor format) as the content format. Using html is simpler for programmatically generated content. Note that Ghost will convert your HTML to its internal format during import.

Expected result: Running the script prints the 5 most recent Ghost post titles with their slugs and publish dates, plus all available tags.

4

Build a Node.js Ghost Integration

The official @tryghost/admin-api and @tryghost/content-api npm packages handle JWT generation and HTTP calls automatically, making Ghost API integration in Node.js very clean. Install them with npm install @tryghost/admin-api @tryghost/content-api express in the Replit Shell. The Ghost JavaScript SDKs abstract away the JWT token generation for Admin API calls and provide a clean Promise-based interface. The example below uses both SDKs in an Express server that exposes read endpoints via the Content API and write endpoints via the Admin API. Deploy with Autoscale for web applications built on Ghost content. If you are running an automation script that publishes content on a schedule, use a Scheduled deployment instead.

server.js
1const express = require('express');
2const GhostContentAPI = require('@tryghost/content-api');
3const GhostAdminAPI = require('@tryghost/admin-api');
4
5const app = express();
6app.use(express.json());
7
8const GHOST_URL = process.env.GHOST_URL;
9const CONTENT_KEY = process.env.GHOST_CONTENT_API_KEY;
10const ADMIN_KEY = process.env.GHOST_ADMIN_API_KEY;
11
12const contentApi = new GhostContentAPI({
13 url: GHOST_URL,
14 key: CONTENT_KEY,
15 version: 'v5.0'
16});
17
18const adminApi = new GhostAdminAPI({
19 url: GHOST_URL,
20 key: ADMIN_KEY,
21 version: 'v5.0'
22});
23
24// GET /posts β€” list recent posts
25app.get('/posts', async (req, res) => {
26 try {
27 const posts = await contentApi.posts.browse({
28 limit: parseInt(req.query.limit) || 10,
29 include: 'tags,authors',
30 order: 'published_at desc'
31 });
32 res.json(posts);
33 } catch (err) {
34 res.status(500).json({ error: err.message });
35 }
36});
37
38// GET /posts/:slug β€” fetch a single post
39app.get('/posts/:slug', async (req, res) => {
40 try {
41 const post = await contentApi.posts.read(
42 { slug: req.params.slug },
43 { include: 'tags,authors' }
44 );
45 res.json(post);
46 } catch (err) {
47 if (err.response?.status === 404) {
48 return res.status(404).json({ error: 'Post not found' });
49 }
50 res.status(500).json({ error: err.message });
51 }
52});
53
54// GET /tags β€” list all tags
55app.get('/tags', async (req, res) => {
56 try {
57 const tags = await contentApi.tags.browse({ limit: 'all' });
58 res.json(tags);
59 } catch (err) {
60 res.status(500).json({ error: err.message });
61 }
62});
63
64// POST /admin/posts β€” create a new post
65app.post('/admin/posts', async (req, res) => {
66 const { title, html, status = 'draft', tags = [] } = req.body;
67 if (!title || !html) {
68 return res.status(400).json({ error: 'title and html are required' });
69 }
70 try {
71 const post = await adminApi.posts.add(
72 { title, html, status, tags: tags.map(name => ({ name })) },
73 { source: 'html' }
74 );
75 res.status(201).json(post);
76 } catch (err) {
77 res.status(500).json({ error: err.message });
78 }
79});
80
81app.listen(3000, '0.0.0.0', () => {
82 console.log('Ghost API server running on port 3000');
83});

Pro tip: The Ghost Content API SDK sorts results by published date descending by default. Use the order parameter to sort by other fields β€” for example, order: 'title asc' for alphabetical listing.

Expected result: The server starts on port 3000. A GET to /posts returns the 10 most recent published posts from your Ghost blog.

Common use cases

Custom Headless Blog Frontend

Build a Replit-hosted web application that fetches Ghost posts via the Content API and renders them with a fully custom design β€” one that may not be achievable with standard Ghost themes. Your Replit frontend calls its own backend, which proxies Ghost Content API requests and adds server-side rendering or caching.

Replit Prompt

Create a Flask web app with a / route that fetches the 10 most recent posts from the Ghost Content API (title, excerpt, slug, published_at, feature_image), renders them in an HTML template with Tailwind CSS styling, and includes pagination for older posts.

Copy this prompt to try it in Replit

Automated Post Publishing from External Sources

Build a Replit backend that monitors an external source β€” a newsletter inbox, a social feed, or a content pipeline β€” and automatically creates Ghost draft posts with the Admin API. Human editors then review and publish the drafts. This accelerates content workflows without fully automating publishing.

Replit Prompt

Write a Python script that reads a CSV file of post titles and markdown content, authenticates with the Ghost Admin API using a JWT generated from GHOST_ADMIN_API_KEY, and creates each row as a draft post in Ghost with status 'draft' and a specified tag.

Copy this prompt to try it in Replit

Content Aggregator with Ghost as Source

Use Ghost as the content backend for a larger application β€” for example, an app that aggregates blog posts from multiple sources and displays them in a unified feed. Your Replit backend periodically fetches new Ghost posts, processes them (extracting keywords, generating summaries), and stores them in your app database.

Replit Prompt

Build a Node.js script that fetches all Ghost posts published in the last 7 days using the Content API with the filter parameter, strips HTML from the mobiledoc content, and stores each post's title, slug, and plain-text excerpt in a PostgreSQL database.

Copy this prompt to try it in Replit

Troubleshooting

Content API returns 401 Unauthorized or empty posts array

Cause: The Content API key is incorrect, the Ghost URL has a trailing slash or wrong protocol, or no posts have been published in the Ghost instance.

Solution: Verify GHOST_URL in Replit Secrets has no trailing slash and uses https://. Copy the Content API key directly from Ghost Admin > Integrations. Make sure at least one post is published (not just saved as draft) in your Ghost admin. Test by opening GHOST_URL/ghost/api/content/posts/?key=YOUR_KEY in a browser.

typescript
1# Python: test Content API URL directly
2test_url = f"{os.environ['GHOST_URL']}/ghost/api/content/posts/"
3params = {'key': os.environ['GHOST_CONTENT_API_KEY'], 'limit': 1}
4print(requests.get(test_url, params=params).json())

Admin API JWT generation fails with 'Invalid token' error

Cause: The Admin API key is not in the correct ID:SECRET format, the secret is not valid hexadecimal, or the JWT expiry is too short or the server clock is skewed.

Solution: Verify the GHOST_ADMIN_API_KEY value is the full key string in ID:SECRET format as shown in Ghost Admin > Integrations. The secret portion must be a valid hex string. If using the Python JWT approach, ensure the system clock is reasonably accurate β€” Ghost validates JWT iat and exp claims with a small tolerance.

typescript
1# Python: validate Admin API key format
2admin_key = os.environ['GHOST_ADMIN_API_KEY']
3parts = admin_key.split(':')
4print(f'Parts: {len(parts)}') # Should be 2
5print(f'ID length: {len(parts[0])}')
6print(f'Secret is hex: {all(c in "0123456789abcdef" for c in parts[1].lower())}')

Ghost Admin API returns 409 Conflict when updating a post

Cause: The Ghost Admin API uses optimistic concurrency β€” you must pass the post's current updated_at timestamp in your update request to prevent overwriting concurrent edits.

Solution: Fetch the post first to get its current updated_at value, then include that exact value in your update request body. If another edit occurred between your fetch and update, Ghost returns 409 β€” implement retry logic that re-fetches the post and tries the update again.

typescript
1# Python: fetch before update to get updated_at
2current = requests.get(f"{GHOST_URL}/ghost/api/admin/posts/{post_id}/", headers=_admin_headers()).json()['posts'][0]
3updated_at = current['updated_at']
4# Now update with the correct updated_at
5body = {'posts': [{'updated_at': updated_at, 'title': 'New Title'}]}

Best practices

  • Store GHOST_CONTENT_API_KEY and GHOST_ADMIN_API_KEY in Replit Secrets (lock icon πŸ”’) β€” the Admin API key especially grants full write access and must never be exposed to clients.
  • Use the Content API for any public-facing read operations β€” it is designed for this and is safe to cache aggressively, as its content only changes when posts are published.
  • Always include updated_at in Ghost Admin API PUT requests to avoid 409 conflict errors from concurrent edits β€” fetch the post first to get the current value.
  • Use Ghost's filter parameter for efficient server-side filtering rather than fetching all posts and filtering in your application code.
  • When creating posts programmatically, use status: 'draft' until you have verified the content looks correct in the Ghost editor β€” accidentally publishing unreviewed content is difficult to undo at scale.
  • Cache Ghost Content API responses in a key-value store with a TTL matching your publishing frequency β€” Ghost content does not change more often than you publish.
  • Deploy with Autoscale for web frontends built on Ghost content; use Scheduled deployment for automation scripts that publish or import content on a regular cadence.
  • Use the include parameter to fetch related data (tags, authors) in a single request rather than making separate API calls β€” Ghost's Content API supports include=tags,authors on most endpoints.

Alternatives

Frequently asked questions

How do I store Ghost API keys in Replit?

Click the lock icon πŸ”’ in the Replit sidebar to open Secrets. Add GHOST_URL with your Ghost site URL (no trailing slash), GHOST_CONTENT_API_KEY with your Content API key, and GHOST_ADMIN_API_KEY with your Admin API key in ID:SECRET format. Find these values in Ghost Admin under Settings > Integrations.

What is the difference between the Ghost Content API and Admin API?

The Content API is read-only and designed for public content delivery β€” use it to fetch published posts, pages, tags, and authors. It uses a simple API key as a query parameter. The Admin API provides full CRUD access including creating posts, managing members, and sending newsletters. It uses a signed JWT for authentication and should only be called from your server-side Replit code.

Does Ghost work with Replit for free?

Yes. Ghost has a free self-hosted option (hosted on your own server) and Ghost Pro starts at $9/month. The Ghost APIs work the same whether self-hosted or on Ghost Pro. Replit's free tier supports outbound API calls, so you can build and test Ghost integrations without a paid Replit plan. Always-on webhook receivers require Replit Core for deployed apps.

How do I create a Ghost post from Replit?

Use the Ghost Admin API with a POST request to /ghost/api/admin/posts/. You need to generate a JWT from your Admin API key (ID:SECRET format) and include it in the Authorization: Ghost TOKEN header. The post body should include title, html (or lexical), and optionally status (draft or published) and tags. The official @tryghost/admin-api npm package handles JWT generation automatically.

Can I use Ghost as a headless CMS with a Replit frontend?

Yes β€” Ghost is one of the best headless CMS options. Enable the Content API in Ghost Admin > Integrations, then fetch posts from your Replit backend using the Content API and render them with your own template. This approach gives you full control over design and performance while using Ghost for content management.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation β€” no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.