Ghost Integration

Ghost Integration

Ghost Integration

Connect your Ghost publication to Wrodium for AI-powered content optimization.

Created date:

Dec 5, 2025

Updated date:

Dec 11, 2025

Prerequisites

  • Ghost 4.0 or later (self-hosted or Ghost Pro)

  • Admin access to your Ghost site

  • Custom Integration created in Ghost

Setup Steps

Step 1: Create a Custom Integration

  1. Log into your Ghost Admin panel

  2. Go to Settings → Integrations

  3. Scroll to Custom Integrations

  4. Click Add custom integration

  5. Name it "Wrodium" and click Create

Step 2: Get Your Admin API Key

After creating the integration, you'll see:

  • Admin API Key: A long string in format {id}:{secret}

  • API URL: Your Ghost admin URL

Copy the Admin API Key — you'll need both parts (id and secret).

Step 3: Configure Wrodium

  1. Go to Settings → CMS Connection in Wrodium

  2. Select Ghost as your provider

  3. Enter your configuration:

Field

Value

Example

Admin URL

Your Ghost site URL

https://yourblog.com

Admin API Key

The full key with colon

64a1b2c3d4e5:a1b2c3d4...

API Version

Ghost API version

v5.0

  1. Click Test Connection then Save

Configuration Schema

{
  "provider": "ghost",
  "provider_config": {
    "admin_url": "https://yourblog.com",
    "admin_api_key": "64a1b2c3d4e5f6a1b2c3d4e5:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
    "version": "v5.0"
  }
}

API Behavior

Authentication

Ghost Admin API uses JWT authentication. Wrodium automatically:

  1. Splits your API key into {key_id}:{secret}

  2. Generates a short-lived JWT token

  3. Signs requests with Ghost {token} header

Listing Articles

Wrodium fetches posts via:


Updating Articles

Updates require the current updated_at timestamp (collision detection):

PUT /ghost/api/admin/posts/{id}/
Body:
{
  "posts": [{
    "id": "post-id",
    "updated_at": "2024-01-15T10:30:00.000Z",
    "title": "Updated Title",
    "html": "<p>Updated content...</p>",
    "excerpt": "Updated excerpt",
    "status": "published"
  }]

Content Format

Ghost uses HTML for post content by default. Wrodium sends optimized content as HTML.

Supported HTML elements:

  • Headings: <h1> through <h6>

  • Paragraphs: <p>

  • Lists: <ul>, <ol>, <li>

  • Links: <a href="...">

  • Images: <img src="..." alt="...">

  • Formatting: <strong>, <em>, <code>

  • Blockquotes: <blockquote>

  • Code blocks: <pre><code>...</code></pre>

Lexical Format (Ghost 5.0+)

Ghost 5.0+ also supports Lexical JSON format. If your Ghost uses Lexical:

{
  "posts": [{
    "id": "post-id",
    "updated_at": "...",
    "lexical": "{\"root\":{\"children\":[...]}}"
  }]
}

Wrodium can convert HTML to Lexical format when needed.

Post Status

Ghost Status

Description

published

Live on your site

draft

Not published

scheduled

Will publish at set time

sent

Email newsletter sent

When updating, set "status": "published" to keep content live.

API Version Compatibility

Ghost Version

API Version

Notes

4.x

v4.0

HTML content only

5.x

v5.0

Lexical or HTML

5.70+

v5.0

Enhanced API

Troubleshooting

"401 Unauthorized" Error

  • Verify the Admin API Key format: {id}:{secret}

  • Check that the integration is still active in Ghost

  • Ensure you're using the Admin API key, not the Content API key

"404 Not Found" Error

  • Verify the Admin URL doesn't have a trailing slash

  • Ensure the Ghost version matches the API version

"409 Conflict" Error (UpdateCollision)

  • The post was modified since you fetched it

  • Wrodium will automatically retry with the latest updated_at

"422 Validation" Error

  • HTML content may contain unsupported tags

  • Check post slug format (lowercase, hyphens only)

SSL/TLS Errors (Self-Hosted)

  • Ensure your Ghost site has a valid SSL certificate

  • For self-signed certs, you may need to add exceptions

Code Example

import time
import jwt
import httpx

class GhostClient:
    def __init__(self, admin_url: str, api_key: str, version: str = "v5.0"):
        self.admin_url = admin_url.rstrip("/")
        self.api_key = api_key
        self.version = version
    
    def _make_token(self):
        key_id, secret = self.api_key.split(":", 1)
        now = int(time.time())
        payload = {
            "iat": now,
            "exp": now + 5 * 60,  # 5 minute expiry
            "aud": "/admin/",
        }
        header = {"alg": "HS256", "kid": key_id, "typ": "JWT"}
        secret_bytes = bytes.fromhex(secret)
        return jwt.encode(payload, secret_bytes, algorithm="HS256", headers=header)
    
    def _headers(self):
        return {
            "Authorization": f"Ghost {self._make_token()}",
            "Accept-Version": self.version,
            "Content-Type": "application/json",
        }
    
    def _base(self):
        return f"{self.admin_url}/ghost/api/admin"
    
    async def list_articles(self, limit: int = 20):
        url = f"{self._base()}/posts/"
        params = {
            "limit": limit,
            "fields": "id,title,slug,excerpt,html,created_at,updated_at,status,url",
        }
        async with httpx.AsyncClient(timeout=15) as client:
            resp = await client.get(url, headers=self._headers(), params=params)
            resp.raise_for_status()
        return resp.json().get("posts", [])
    
    async def update_article(self, article_id: str, updates: dict):
        # Get current post for updated_at
        get_url = f"{self._base()}/posts/{article_id}/"
        async with httpx.AsyncClient(timeout=15) as client:
            current = await client.get(get_url, headers=self._headers())
            current.raise_for_status()
        current_post = current.json()["posts"][0]
        
        payload_post = {
            "id": article_id,
            "updated_at": current_post["updated_at"],  # Required for collision detection
        }
        
        if "title" in updates:
            payload_post["title"] = updates["title"]
        if "content" in updates:
            payload_post["html"] = updates["content"]
        if "excerpt" in updates:
            payload_post["excerpt"] = updates["excerpt"]
        if "status" in updates:
            payload_post["status"] = updates["status"]
        
        async with httpx.AsyncClient(timeout=15) as client:
            resp = await client.put(
                get_url,
                headers=self._headers(),
                json={"posts": [payload_post]}
            )
            resp.raise_for_status()
        return resp.json()["posts"][0]

Required Dependencies

For JWT token generation:

Webhooks (Optional)

Ghost can send webhooks when content changes:

  1. Go to Settings → Integrations → Your Integration

  2. Add a webhook:

    • Event: Post published

    • Target URL: https://api.wrodium.com/webhooks/ghost/{your_brand_id}

Next Steps

Found this article insightful? Spread the words on…

Found this article insightful?
Spread the words on…

X.com

Share on X

X.com

Share on X

X.com

Share on X

X.com

Share on LinkedIn

X.com

Share on LinkedIn

X.com

Share on LinkedIn

Found this documentation insightful? Share it on…

X.com

LinkedIn

Contents

Checkout other documentations

Checkout other documentations

Checkout other documentations

Let us help you win on

ChatGPT

Let us help you win on

ChatGPT

Let us help you win on

ChatGPT