A modern, modular Python wrapper for the Ghost Admin API. PyGhost makes it incredibly easy to interact with your Ghost CMS programmatically.
- Simple & Intuitive: Easy-to-use API with clear method names
- Modular Design: Feature-based modules for better organization
- Complete API Coverage: Posts, Pages, Tiers, Newsletters, Offers, Members, Users, Images, Themes, and Webhooks
- Full CRUD Operations: Create, read, update, delete for all content types
- Flexible Content: Support for both Lexical JSON and HTML content
- Subscription Management: Complete tier and pricing management
- Newsletter Configuration: Email styling and sender management
- Discount Offers: Promotional codes and discount management
- Member Management: Subscriber CRUD operations and subscription tracking
- User Administration: Site staff and author management with roles
- Image Upload: Multipart file uploads and media management
- Theme Management: Theme upload, activation, and validation
- Webhook Integration: Event-driven webhook management and monitoring
- Tag & Author Management: Easy handling of tags and authors
- Robust Error Handling: Comprehensive exception handling
- JWT Authentication: Secure token-based authentication
- Type Hints: Full typing support for better development experience
- Type Safety: Comprehensive enums for all fixed options with IDE autocompletion
- Sphinx Documentation: Auto-generated API documentation support
pip install -r requirements.txt
from pyghost import GhostClient
# Initialize the client
client = GhostClient(
site_url="https://your-site.ghost.io",
admin_api_key="your_admin_key:your_secret"
)
# Create a new post with type-safe enums
from pyghost.enums import PostStatus, ContentType
post = client.posts.create(
title="My First Post",
content="<p>Hello, World!</p>",
status=PostStatus.PUBLISHED,
content_type=ContentType.HTML,
tags=["python", "ghost", "api"]
)
# Create a page
page = client.pages.create(
title="About Us",
content="<h1>About Our Company</h1><p>We are...</p>",
slug="about"
)
# Create a subscription tier with type-safe enums
from pyghost.enums import Currency, TierVisibility
tier = client.tiers.create(
name="Premium Plan",
monthly_price=999, # $9.99 in cents
yearly_price=9999, # $99.99 in cents
currency=Currency.USD,
visibility=TierVisibility.PUBLIC,
benefits=["Access to all content", "Priority support"]
)
# Create a member (subscriber)
member = client.members.create(
email="subscriber@example.com",
name="John Subscriber",
labels=["VIP", "Newsletter"]
)
# Upload an image
image_result = client.images.upload("path/to/image.jpg")
print(f"Created post: {post['title']}, member: {member['name']}, image: {image_result['url']}")
PyGhost provides comprehensive enums for all fixed options, ensuring type safety and preventing typos:
from pyghost.enums import (
PostStatus, PageStatus, ContentType,
OfferType, OfferDuration, OfferStatus,
NewsletterStatus, NewsletterVisibility,
TierVisibility, Currency,
WebhookEvent, WebhookStatus,
MemberStatus, UserRole, UserStatus
)
# Create content with type-safe enums
post = client.posts.create(
title="My Blog Post",
content="<p>Content here</p>",
status=PostStatus.PUBLISHED,
content_type=ContentType.HTML
)
# Create offers with enums
offer = client.offers.create(
name="Black Friday Sale",
code="BLACKFRIDAY50",
type=OfferType.PERCENT,
amount=50,
duration=OfferDuration.ONCE,
tier="premium-tier-id"
)
# Create newsletters with enums
newsletter = client.newsletters.create(
name="Weekly Updates",
status=NewsletterStatus.ACTIVE,
visibility=NewsletterVisibility.MEMBERS
)
# Create tiers with currency enums
tier = client.tiers.create(
name="Pro Plan",
monthly_price=1999, # $19.99
currency=Currency.USD,
visibility=TierVisibility.PUBLIC
)
# Create webhooks with event enums
webhook = client.webhooks.create(
event=WebhookEvent.POST_PUBLISHED,
target_url="https://example.com/webhook"
)
# Filter users by role
editors = client.users.get_editors()
authors = client.users.get_authors()
# Filter members by status
paid_members = client.members.get_paid_members()
free_members = client.members.get_free_members()
- PostStatus:
DRAFT
,PUBLISHED
,SCHEDULED
,SENT
- PageStatus:
DRAFT
,PUBLISHED
,SCHEDULED
- ContentType:
LEXICAL
,HTML
- OfferType:
PERCENT
,FIXED
- OfferDuration:
ONCE
,FOREVER
,REPEATING
- NewsletterStatus:
ACTIVE
,ARCHIVED
- TierVisibility:
PUBLIC
,NONE
- WebhookEvent:
POST_ADDED
,POST_DELETED
,POST_PUBLISHED
, etc. - MemberStatus:
FREE
,PAID
,COMPED
- UserRole:
OWNER
,ADMINISTRATOR
,EDITOR
,AUTHOR
,CONTRIBUTOR
- Currency:
USD
,EUR
,GBP
,CAD
,AUD
, etc.
To use PyGhost, you need a Ghost Admin API key:
- Go to your Ghost Admin panel
- Navigate to Settings β Integrations
- Create a new Custom Integration
- Copy the Admin API Key
The API key format is: key_id:secret_hex_string
client = GhostClient(
site_url="https://your-ghost-site.com",
admin_api_key="507f1f77bcf86cd799439011:3c5416c8b27c4e71ba2e1a2ac9c8f4d7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3"
)
The Posts module handles blog post management with full CRUD operations, publishing, and scheduling.
Basic Post Creation:
from pyghost.enums import PostStatus, ContentType
# Create a draft post with Lexical content
post = client.posts.create(
title="My New Post",
content='{"root":{"children":[...]}}', # Lexical JSON
content_type=ContentType.LEXICAL
)
# Create a post with HTML content
post = client.posts.create(
title="HTML Post",
content="<p>Hello, <strong>World!</strong></p>",
content_type=ContentType.HTML,
status=PostStatus.PUBLISHED
)
Advanced Post Creation:
from pyghost.enums import PostStatus, ContentType
post = client.posts.create(
title="Advanced Post",
content="Post content here...",
content_type=ContentType.LEXICAL,
status=PostStatus.PUBLISHED,
tags=["tutorial", "python"],
authors=["author@example.com"],
excerpt="This is a great post about PyGhost",
featured=True,
feature_image="https://example.com/image.jpg",
meta_title="SEO Title",
meta_description="SEO description"
)
# Get a specific post by ID
post = client.posts.get("post_id_here")
# Get a post by slug
post = client.posts.get_by_slug("my-post-slug")
# List all posts
posts = client.posts.list()
# List with filtering and pagination
posts = client.posts.list(
limit=10,
page=1,
filter_="status:published",
include="tags,authors",
order="published_at desc"
)
# Get the current post to get updated_at timestamp
current_post = client.posts.get("post_id")
# Update the post
updated_post = client.posts.update(
post_id="post_id",
updated_at=current_post["updated_at"],
title="New Title",
content="Updated content...",
tags=["updated", "post"]
)
# Publish a post immediately
client.posts.publish("post_id", updated_at)
# Schedule a post for later
from datetime import datetime, timedelta
future_date = datetime.now() + timedelta(days=1)
client.posts.schedule("post_id", updated_at, future_date)
# Unpublish a post (revert to draft)
client.posts.unpublish("post_id", updated_at)
# Delete a post permanently
client.posts.delete("post_id")
The Pages module manages static pages with similar functionality to posts.
from pyghost.enums import PageStatus
# Create a basic page
page = client.pages.create(
title="About Us",
content="<h1>About Our Company</h1><p>We are a leading...</p>",
slug="about",
meta_title="About Us - Company Information",
meta_description="Learn more about our company"
)
# Create a page with custom settings
page = client.pages.create(
title="Privacy Policy",
content="<h1>Privacy Policy</h1><p>Your privacy is important...</p>",
slug="privacy",
status=PageStatus.PUBLISHED,
featured=True,
visibility="public"
)
# List all pages
pages = client.pages.list()
# Get a specific page
page = client.pages.get("page_id")
page = client.pages.get_by_slug("about")
# Update a page
updated_page = client.pages.update(
page_id="page_id",
title="Updated About Us",
updated_at=page["updated_at"]
)
# Publish/unpublish pages
client.pages.publish("page_id")
client.pages.unpublish("page_id")
The Tiers module manages subscription tiers and pricing.
from pyghost.enums import Currency, TierVisibility
# Create a basic paid tier
tier = client.tiers.create(
name="Premium Plan",
description="Perfect for professionals",
monthly_price=1999, # $19.99 in cents
yearly_price=19999, # $199.99 in cents
currency=Currency.USD,
visibility=TierVisibility.PUBLIC,
benefits=[
"Access to all premium content",
"Priority support",
"Monthly video calls"
]
)
# Create a free tier
free_tier = client.tiers.create(
name="Free Membership",
description="Get started for free",
monthly_price=0,
yearly_price=0,
currency=Currency.USD,
benefits=["Access to free posts"]
)
# List all tiers
tiers = client.tiers.list()
# Get active tiers only
active_tiers = client.tiers.get_active_tiers()
# Update tier pricing
updated_tier = client.tiers.update(
tier_id="tier_id",
monthly_price=2499, # Increase to $24.99
benefits=[
"Everything in previous version",
"New premium feature"
]
)
# Archive/activate tiers
client.tiers.archive("tier_id")
client.tiers.activate("tier_id")
The Newsletters module manages email newsletter configuration and styling.
from pyghost.enums import NewsletterStatus, NewsletterSenderReplyTo
# Create a newsletter with custom styling
newsletter = client.newsletters.create(
name="Weekly Digest",
description="Our weekly roundup of content",
sender_name="Editorial Team",
sender_reply_to=NewsletterSenderReplyTo.NEWSLETTER,
status=NewsletterStatus.ACTIVE,
title_font_category="serif",
title_alignment="center",
show_badge=False,
subscribe_on_signup=True
)
# Create a minimal newsletter
minimal_newsletter = client.newsletters.create(
name="Product Updates",
description="Important product announcements",
sender_name="Product Team",
show_header_icon=False,
show_feature_image=False,
subscribe_on_signup=False
)
# List newsletters
newsletters = client.newsletters.list()
active_newsletters = client.newsletters.get_active_newsletters()
# Update newsletter styling
updated_newsletter = client.newsletters.update(
newsletter_id="newsletter_id",
title_alignment="left",
body_font_category="sans_serif",
footer_content="<p>Thanks for reading!</p>"
)
# Archive/activate newsletters
client.newsletters.archive("newsletter_id")
client.newsletters.activate("newsletter_id")
The Offers module manages discount offers and promotional codes.
from pyghost.enums import OfferType, OfferDuration, OfferCadence, Currency
# Create a percentage discount
percent_offer = client.offers.create(
name="Black Friday Sale",
code="BLACKFRIDAY2024",
display_title="50% Off Annual Plans",
display_description="Limited time - 50% off all annual subscriptions",
type=OfferType.PERCENT,
amount=50, # 50% discount
duration=OfferDuration.ONCE, # Apply to first payment only
tier_id="tier_id",
cadence=OfferCadence.YEAR
)
# Create a fixed amount discount
fixed_offer = client.offers.create(
name="Welcome Bonus",
code="WELCOME10",
display_title="$10 Off Your First Month",
display_description="New customer special offer",
type=OfferType.FIXED,
amount=1000, # $10.00 in cents
duration=OfferDuration.ONCE,
tier_id="tier_id",
cadence=OfferCadence.MONTH,
currency=Currency.USD
)
# List offers
offers = client.offers.list(include="tier")
active_offers = client.offers.get_active_offers()
# Get offer by code
offer = client.offers.get_by_code("BLACKFRIDAY2024")
# Update offer
updated_offer = client.offers.update(
offer_id="offer_id",
display_title="Extended: 50% Off!",
amount=60 # Increase discount to 60%
)
# Calculate discount
calculation = client.offers.calculate_discount(offer, 2999) # $29.99
print(f"Final price: ${calculation['final_price']/100:.2f}")
# Generate offer URL
offer_url = client.offers.generate_offer_url("BLACKFRIDAY2024", "https://mysite.com")
The Members module manages subscribers and their relationships with newsletters, tiers, and labels.
# Create a basic member
member = client.members.create(
email="subscriber@example.com",
name="John Subscriber"
)
# Create a member with labels and newsletter subscriptions
advanced_member = client.members.create(
email="premium@example.com",
name="Premium Subscriber",
note="VIP customer from email campaign",
labels=["VIP", "Premium", "Newsletter"],
newsletters=["newsletter_id_1", "newsletter_id_2"],
tiers=["tier_id_1"]
)
# List all members
members = client.members.list()
# Get members with filtering
paid_members = client.members.list(
filter_="status:paid",
include="newsletters,tiers",
order="created_at desc"
)
# Get a specific member
member = client.members.get("member_id", include="labels,newsletters")
member = client.members.get_by_email("subscriber@example.com")
# Update member information
updated_member = client.members.update(
member_id="member_id",
name="Updated Name",
note="Updated customer status",
labels=["Premium", "Updated"]
)
# Add labels to a member (preserves existing labels)
client.members.add_labels("member_id", ["New Label", "Another Label"])
# Remove specific labels
client.members.remove_labels("member_id", ["Old Label", "Expired"])
# Get members by label
vip_members = client.members.get_members_by_label("VIP")
# Subscribe to additional newsletters
client.members.subscribe_to_newsletters(
"member_id",
["newsletter_id_1", "newsletter_id_2"]
)
# Unsubscribe from newsletters
client.members.unsubscribe_from_newsletters(
"member_id",
["newsletter_id_1"]
)
# Get member statistics
stats = client.members.get_member_statistics()
print(f"Total: {stats['total']}, Paid: {stats['paid']}, Free: {stats['free']}")
# Get different member types
paid_members = client.members.get_paid_members(include="tiers")
free_members = client.members.get_free_members()
The Users module manages site staff and authors (different from members/subscribers).
# List all users
users = client.users.list(include="count.posts,roles")
# Get a specific user
user = client.users.get("user_id", include="count.posts,roles")
user = client.users.get_by_email("author@example.com")
user = client.users.get_by_slug("author-slug")
# Update user profile
updated_user = client.users.update(
user_id="user_id",
name="John Author",
bio="Senior content writer with 5 years experience",
website="https://johnauthor.com",
location="San Francisco, CA",
twitter="@johnauthor",
meta_title="John Author - Technical Writer",
meta_description="Experienced technical writer and content creator"
)
# Get users by role
owners = client.users.get_owners()
administrators = client.users.get_administrators()
editors = client.users.get_editors()
authors = client.users.get_authors(include="count.posts")
contributors = client.users.get_contributors()
# Get available roles
roles = client.users.get_user_roles()
for role in roles:
print(f"Role: {role['name']} - {role['description']}")
# Get user permissions
permissions = client.users.get_user_permissions("user_id")
# Update notification preferences
client.users.update_notification_settings(
"user_id",
comment_notifications=True,
mention_notifications=True,
milestone_notifications=False,
free_member_signup_notification=True,
paid_subscription_started_notification=True
)
# Get user statistics
stats = client.users.get_user_statistics()
print(f"Total Users: {stats['total']}, Authors: {stats['authors']}")
# Get active users
active_users = client.users.get_active_users()
The Images module handles file uploads and media management.
# Upload from file path
result = client.images.upload("/path/to/image.jpg")
print(f"Image URL: {result['url']}")
print(f"Reference: {result['ref']}")
# Upload from file object
with open("image.png", "rb") as f:
result = client.images.upload(f, "image.png")
# Upload multiple images
image_paths = ["/path/to/image1.jpg", "/path/to/image2.png"]
results = client.images.upload_multiple(image_paths)
# Upload from URL
result = client.images.upload_from_url(
"https://example.com/image.jpg",
"my-image.jpg"
)
# Upload without validation (use with caution)
result = client.images.upload("image.jpg", validate=False)
# Validate a single image
is_valid = client.images.validate_image_path("/path/to/image.jpg")
# Get detailed image information
info = client.images.get_image_info("/path/to/image.jpg")
print(f"Size: {info['size_mb']} MB, Format: {info['format']}")
print(f"Dimensions: {info['width']}x{info['height']}")
print(f"Supported: {info['is_supported']}")
# Batch validation
files = ["/path/to/image1.jpg", "/path/to/image2.png"]
results = client.images.batch_validate(files)
print(f"Valid: {results['valid_count']}, Invalid: {results['invalid_count']}")
# Supported image formats
supported_formats = client.images.SUPPORTED_FORMATS
print(f"Supported formats: {', '.join(sorted(supported_formats))}")
# Maximum file size
max_size_mb = client.images.MAX_FILE_SIZE / (1024 * 1024)
print(f"Maximum file size: {max_size_mb} MB")
The Themes module handles Ghost theme upload, activation, and management.
# Upload a theme from file path
uploaded_theme = client.themes.upload_from_file(
"/path/to/my-theme.zip",
activate=True # Activate immediately after upload
)
# Upload from file object
with open("theme.zip", "rb") as theme_file:
theme = client.themes.upload_from_file_object(theme_file)
# Activate an uploaded theme
activated_theme = client.themes.activate("theme-name")
# Validate theme structure before upload
validation = client.themes.validate_theme_structure("/path/to/theme.zip")
print(f"Valid: {validation['valid']}")
print(f"Templates: {len(validation['templates'])}")
# Get theme information
info = client.themes.get_theme_info("/path/to/theme.zip")
print(f"Name: {info['name']}")
print(f"Version: {info['version']}")
print(f"Description: {info['description']}")
# Check supported formats
formats = client.themes.get_supported_formats()
is_valid = client.themes.validate_theme_format("theme.zip")
The Webhooks module manages event-driven webhooks for real-time integrations.
from pyghost.enums import WebhookEvent
# Create a basic webhook
webhook = client.webhooks.create(
event=WebhookEvent.POST_PUBLISHED,
target_url="https://example.com/webhook",
name="Post Publication Hook"
)
# Create webhook with security
secure_webhook = client.webhooks.create(
event=WebhookEvent.MEMBER_ADDED,
target_url="https://example.com/new-member",
name="New Member Webhook",
secret="your-secret-key",
api_version="v5.0"
)
# Use convenience methods (accepts both enums and strings)
post_hook = client.webhooks.create_post_webhook(
"https://example.com/post-hook",
event=WebhookEvent.POST_EDITED
)
member_hook = client.webhooks.create_member_webhook(
"https://example.com/member-hook"
)
# List all webhooks
webhooks = client.webhooks.list()
# Get specific webhook
webhook = client.webhooks.get("webhook_id")
# Update webhook
updated = client.webhooks.update(
webhook_id="webhook_id",
target_url="https://new-endpoint.com/webhook",
name="Updated Webhook Name"
)
# Delete webhook
success = client.webhooks.delete("webhook_id")
# Bulk delete webhooks
results = client.webhooks.bulk_delete_webhooks([
"webhook_id_1", "webhook_id_2"
])
# Get available events
events = client.webhooks.get_webhook_events()
print(f"Available events: {', '.join(events)}")
# Validate event types
is_valid = client.webhooks.validate_webhook_event("post.published")
# Get webhooks by event
post_webhooks = client.webhooks.get_webhooks_by_event("post.published")
# Get webhook statistics
stats = client.webhooks.get_webhook_statistics()
print(f"Total: {stats['total']}, Active: {stats['active']}")
# Get active webhooks only
active = client.webhooks.get_active_webhooks()
# Using tag names (will create tags if they don't exist)
post = client.posts.create(
title="Tagged Post",
tags=["python", "tutorial", "beginner"]
)
# Using tag objects for more control
post = client.posts.create(
title="Tagged Post",
tags=[
{"name": "python", "description": "Python programming"},
{"name": "#internal"} # Hidden tag
]
)
# Using author emails
post = client.posts.create(
title="Multi-Author Post",
authors=["author1@example.com", "author2@example.com"]
)
# Using author IDs
post = client.posts.create(
title="Post by ID",
authors=[
{"id": "author_id_1"},
{"id": "author_id_2"}
]
)
PyGhost provides specific exception types for different error scenarios:
from pyghost import GhostClient
from pyghost.exceptions import (
GhostAPIError,
AuthenticationError,
ValidationError,
NotFoundError,
RateLimitError
)
try:
client = GhostClient(site_url="...", admin_api_key="...")
post = client.posts.create(title="Test Post")
except AuthenticationError as e:
print(f"Authentication failed: {e}")
except ValidationError as e:
print(f"Validation error: {e}")
except NotFoundError as e:
print(f"Resource not found: {e}")
except RateLimitError as e:
print(f"Rate limit exceeded: {e}")
except GhostAPIError as e:
print(f"API error: {e}")
Ghost's modern content format. You can create Lexical content programmatically:
lexical_content = {
"root": {
"children": [
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "Hello, World!",
"type": "extended-text",
"version": 1
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "root",
"version": 1
}
}
from pyghost.enums import ContentType
post = client.posts.create(
title="Lexical Post",
content=json.dumps(lexical_content),
content_type=ContentType.LEXICAL
)
For traditional HTML content:
from pyghost.enums import ContentType, PostStatus
post = client.posts.create(
title="HTML Post",
content="<p>Hello, <strong>World!</strong></p>",
content_type=ContentType.HTML,
status=PostStatus.PUBLISHED
)
Check out the examples/
directory for comprehensive examples:
- basic_usage.py: Simple post creation and management
- bulk_operations.py: Working with multiple posts efficiently
- pages_usage.py: Complete pages management examples
- tiers_usage.py: Subscription tier and pricing examples
- newsletters_usage.py: Newsletter configuration and styling
- offers_usage.py: Discount offers and promotional codes
- members_usage.py: Member management and subscription tracking
- users_usage.py: User profile and role management examples
- images_usage.py: Image upload and media management examples
- themes_usage.py: Theme upload, validation, and management examples
- webhooks_usage.py: Webhook creation, monitoring, and event handling
Each example file includes:
- Basic CRUD operations
- Advanced features and utilities
- Error handling best practices
- Cleanup procedures
- Production usage tips
- JWT tokens expire after 5 minutes (handled automatically)
- Rate limiting applies (usually 500 requests per hour)
- Some operations require specific permissions
updated_at
timestamp is required for updates to prevent conflicts
# Clone the repository
git clone <repository-url>
cd pyghost
# Install dependencies
pip install -r requirements.txt
# Install in development mode
pip install -e .
# Run tests (when available)
python -m pytest tests/
# Run with coverage
python -m pytest tests/ --cov=pyghost
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the MIT License - see the LICENSE file for details.
- Documentation: Ghost Admin API Docs
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Posts API support β
- Pages API support β
- Tiers API support β
- Newsletters API support β
- Offers API support β
- Members API support β
- Users and roles API β
- Image upload support β
- Themes API support β
- Webhooks API support β
- Site settings API
- Tags API support
- Bulk operations optimization
- Async/await support
- CLI tool
- Enhanced testing suite
Made with β€οΈ for the Ghost community