Connect Retool to Ghost by creating a REST API Resource using the Ghost Admin API key (generated in Ghost Admin → Settings → Integrations). Build content management panels that create and update posts, manage tags and members, track newsletter subscription data, and combine Ghost membership metrics with payment information from Stripe or your billing system.
| Fact | Value |
|---|---|
| Tool | Ghost |
| Category | CMS |
| Method | REST API Resource |
| Difficulty | Beginner |
| Time required | 15 minutes |
| Last updated | April 2026 |
Build a Ghost Content and Membership Management Panel in Retool
Ghost's native admin interface is excellent for individual writers and editors, but publishing operations teams need views it doesn't provide: all drafts across all authors with estimated publish dates, membership churn analysis combining Ghost member data with Stripe payment history, a bulk post-tagger for reorganizing content taxonomies after a site rebrand, or a subscriber health dashboard tracking which newsletter segments have declining open rates. Retool lets you build these operational views directly against the Ghost Admin API.
Ghost Admin API v3 provides full access to posts, pages, tags, authors, members, tiers (membership plans), and email newsletter analytics. Authentication uses a JWT token derived from the Admin API key — Ghost splits the key into an ID and secret, and you use them to sign a short-lived JWT for each request. For simpler setups, Ghost also supports a static API key passed as a URL parameter for the Content API (read-only public data).
The most powerful Retool use case for Ghost is a membership management dashboard that joins Ghost members with Stripe payment data. Publishers can see each member's subscription status, payment history, last login date, and email engagement score in one view — identifying which members are at risk of churning (paid, but haven't logged in or opened newsletters in 30 days) so the retention team can intervene before renewal.
Integration method
Ghost connects to Retool via a REST API Resource using the Ghost Admin API key for authenticated access to post management, member data, and newsletter operations. The Admin API key is split into an ID and secret, combined to generate a JWT token for each request. Retool proxies all requests server-side, keeping API credentials secure and eliminating CORS issues.
Prerequisites
- A Ghost installation (Ghost Pro or self-hosted Ghost 4.0+) with Admin API access
- A Ghost Admin API key (generated from Ghost Admin → Settings → Integrations → Add custom integration)
- For self-hosted Ghost: your Ghost site URL (the domain where Ghost is running)
- A Retool account with permission to create Resources
- Basic familiarity with Retool's query editor and Table component
Step-by-step guide
Generate a Ghost Admin API key and prepare JWT authentication
In Ghost Admin, navigate to Settings (gear icon in the left sidebar). Click Integrations under the Advanced section. Click Add custom integration. Give it a name (e.g., 'Retool Integration') and click Create. Ghost generates an integration entry with two values: Admin API Key and Content API Key. Click the Admin API Key to copy it. The Admin API key format is: ADMIN_KEY_ID:ADMIN_KEY_SECRET, separated by a colon. Split this value — everything before the colon is the key ID, everything after is the secret. For example, if the key is '64abc:def123456789', the ID is '64abc' and the secret is 'def123456789'. Ghost Admin API authentication uses JWT tokens. You must generate a JWT signed with the key secret for each API session. The JWT payload must include: { sub: 'user_email_or_id', iat: current_timestamp_seconds, exp: timestamp_5_minutes_from_now, aud: '/v3/admin/' }. Sign with HS256 using the hex-decoded key secret. For simplicity in Retool, you can generate a long-lived JWT token externally (valid for hours or days) and store it in Retool Configuration Variables. Alternatively, use a Retool JavaScript query to generate a fresh JWT token before each API request using the jose or jsonwebtoken library patterns. Store in Retool Configuration Variables: GHOST_ADMIN_KEY_ID, GHOST_ADMIN_KEY_SECRET (marked as secret), and GHOST_SITE_URL (e.g., https://yourblog.ghost.io).
1// JavaScript query to generate Ghost Admin API JWT2// Run this before making API calls, refresh as needed3const keyId = retoolContext.configVars.GHOST_ADMIN_KEY_ID;4const keySecret = retoolContext.configVars.GHOST_ADMIN_KEY_SECRET;56// Convert hex secret to bytes7const secretBytes = new Uint8Array(keySecret.match(/.{1,2}/g).map(b => parseInt(b, 16)));89// Build JWT manually (simplified — use a proper JWT library in production)10const now = Math.floor(Date.now() / 1000);11const header = btoa(JSON.stringify({ alg: 'HS256', kid: keyId, typ: 'JWT' })).replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_');12const payload = btoa(JSON.stringify({ iat: now, exp: now + 300, aud: '/v3/admin/' })).replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_');1314// Note: Full HMAC signing requires crypto API — use a pre-generated token for initial setup15return `${header}.${payload}.SIGNATURE_PLACEHOLDER`;Pro tip: For initial setup, generate a Ghost Admin API JWT using an online JWT tool (like jwt.io) with your key secret (hex-decoded to bytes) and HS256 algorithm. Set a long expiration (e.g., 24 hours). Store in GHOST_ADMIN_TOKEN config var. Build automated refresh later.
Expected result: You have a Ghost Admin API JWT token stored in Retool Configuration Variables. Your Ghost site URL, key ID, and key secret are also stored as configuration variables.
Create the Ghost Admin API REST Resource in Retool
In Retool, navigate to the Resources tab and click Add Resource. Select REST API from the connector list. Name: Ghost Admin API Base URL: {{ retoolContext.configVars.GHOST_SITE_URL }}/ghost/api/v3/admin For Ghost Pro sites, the URL is typically https://yoursite.ghost.io/ghost/api/v3/admin. For self-hosted Ghost, use your domain: https://yourdomain.com/ghost/api/v3/admin. Authentication: Select Bearer Token. In the Token field, enter {{ retoolContext.configVars.GHOST_ADMIN_TOKEN }}. This references your pre-generated JWT token. Add a default header: Accept-Version: v5.0 (optional, but specifies the Ghost API version you're targeting. Ghost Admin API is backwards compatible but specifying the version prevents unexpected behavior from API updates). Click Create Resource. Test the connection with a GET query to /posts/?limit=5&fields=id,title,status. A successful response returns a JSON object with a posts array. If you get 401, the JWT token is expired or incorrectly formed — regenerate it. Note: Ghost also has a Content API (read-only, public data) at /ghost/api/content/ authenticated with just a content key as a URL parameter: ?key=YOUR_CONTENT_KEY. For dashboards that only need to read published content without accessing member data, the Content API is simpler to set up.
Pro tip: Ghost's Admin API base path changed between versions — use /ghost/api/v3/admin for Ghost 4.x and /ghost/api/admin for Ghost 5.x. Check your Ghost version in Admin → Settings → About to use the correct path.
Expected result: The Ghost Admin API Resource is created. GET /posts/?limit=5&fields=id,title,status returns the 5 most recent posts with their IDs, titles, and publication status.
Query posts and pages with filtering and pagination
Create a getPosts query: GET /posts/ with the following URL parameters: - limit: 25 (Ghost uses 'limit' not 'per_page') - page: {{ Math.floor(table_posts.pagination.offset / 25) + 1 }} - filter: {{ buildFilter.data }} (Ghost uses a structured filter syntax) - fields: id,title,slug,status,published_at,updated_at,featured,excerpt,tags,authors,primary_author - include: tags,authors (embeds tag and author objects in the response) - order: published_at desc Ghost's filter parameter supports a powerful query syntax. For example: - status:published — only published posts - author:john-doe — posts by a specific author slug - tag:newsletter — posts with a specific tag - published_at:>2024-01-01 — posts published after a date - status:[draft,scheduled] — multiple status values Combine with + for AND: status:published+featured:true for published featured posts. Bind filter options to form components: a Status Select with options [All, Draft, Published, Scheduled], an Author Select (populated from GET /authors/), and a Tag Select (populated from GET /tags/). Build the filter string dynamically in a JavaScript query based on selected filter values.
1// Transformer for Ghost posts2const posts = data?.posts || [];3return posts.map(post => ({4 id: post.id,5 title: post.title || '(No Title)',6 slug: post.slug,7 status: post.status,8 author: post.primary_author?.name || post.authors?.[0]?.name || 'Unknown',9 tags: (post.tags || []).map(t => t.name).join(', '),10 published_at: post.published_at ? new Date(post.published_at).toLocaleDateString() : 'Unpublished',11 updated_at: post.updated_at ? new Date(post.updated_at).toLocaleDateString() : '',12 featured: post.featured ? 'Yes' : 'No',13 excerpt: (post.excerpt || '').substring(0, 150),14 edit_url: `${retoolContext.configVars.GHOST_SITE_URL}/ghost/#/editor/post/${post.id}`15}));Pro tip: Use Ghost's include parameter (include: tags,authors) instead of making separate API calls to fetch tag and author names. Without include, Ghost returns only tag and author IDs, requiring additional lookup queries.
Expected result: The posts Table shows all Ghost posts with author, tags, status, and publish date. Filters by status and author work correctly. Clicking a post shows the edit link in Ghost.
Fetch members and build the membership management panel
Create a getMembers query: GET /members/ with parameters: - limit: 25 - page: {{ Math.floor(table_members.pagination.offset / 25) + 1 }} - filter: {{ select_tierFilter.value === 'paid' ? 'status:paid' : select_tierFilter.value === 'free' ? 'status:free' : '' }} - include: tiers,newsletters - order: created_at desc - search: {{ input_memberSearch.value || '' }} Ghost members include: id, name, email, status (free, paid, comped), created_at, last_seen_at (last login), tiers (subscription plan objects with name and billing_period), newsletters (subscribed newsletter lists), email_count, email_opened_count, and email_open_rate. The email_open_rate field is Ghost's computed engagement metric — the ratio of opened emails to total sent, expressed as a percentage. Use this to identify disengaged members. Create a getMemberDetail query: GET /members/{{ table_members.selectedRow.data.id }} to fetch a single member's full profile when clicking a table row. For membership management actions, create updateMember: PUT /members/{{ selected_member_id }} with body containing fields to update. Common updates: changing tiers, updating newsletter subscriptions, or adding a note. Create deleteMember for removing members (use cautiously — deletion is permanent).
1// Transformer for Ghost members2const members = data?.members || [];3const today = new Date();45return members.map(member => {6 const lastSeen = member.last_seen_at ? new Date(member.last_seen_at) : null;7 const daysSinceLogin = lastSeen ? Math.floor((today - lastSeen) / 86400000) : null;8 const isPaid = member.status === 'paid';9 const isAtRisk = isPaid && (daysSinceLogin > 30 || member.email_open_rate < 10);1011 return {12 id: member.id,13 name: member.name || 'Anonymous',14 email: member.email,15 status: member.status,16 tier: member.tiers?.[0]?.name || 'Free',17 billing_period: member.tiers?.[0]?.billing_period || '',18 created_at: new Date(member.created_at).toLocaleDateString(),19 last_seen: lastSeen ? lastSeen.toLocaleDateString() : 'Never',20 days_since_login: daysSinceLogin,21 open_rate: `${Math.round(member.email_open_rate || 0)}%`,22 at_risk: isAtRisk,23 newsletter_count: (member.newsletters || []).length24 };25});Pro tip: Ghost's member search parameter performs a full-text search across name and email. For filtering by engagement metrics (like open_rate < 10), you'll need to filter client-side in a transformer after fetching members — Ghost's filter syntax doesn't support numeric comparisons on computed fields.
Expected result: The members Table shows all Ghost members with subscription tier, last login date, and email open rate. At-risk paid members (low open rate or long absence) are highlighted in the Table.
Build newsletter analytics and post creation workflows
Ghost tracks email performance per newsletter send. Create a getNewsletters query: GET /newsletters/ to fetch all newsletter channels. Each newsletter has: id, name, subscriber_count, open_rate, and feedback_enabled. For post email analytics, fetch individual post email stats: GET /posts/{{ select_post.value }}?include=email. The email object on a post contains: status (sent, failed), recipients (total sent count), opened_count, open_rate, clicked_count, click_rate, and feedback (thumbs up/down counts if feedback is enabled). For post creation from Retool, create a createPost query: POST /posts/ with body: { "posts": [{ "title": "{{ input_title.value }}", "lexical": null, "status": "draft", "tags": [{ "name": "{{ input_tag.value }}" }], "authors": [{ "id": "{{ select_author.value }}" }], "excerpt": "{{ input_excerpt.value }}" }] } Note: Ghost posts require either mobiledoc or lexical content format. Creating with null lexical creates an empty post that editors can then open in Ghost's editor via the edit_url. For bulk operations like adding a tag to multiple posts, create a JavaScript query that loops over selected Table rows and patches each post using PUT /posts/{id}. Add a version timestamp in each update payload: "updated_at": post.updated_at (Ghost uses optimistic locking and requires the current updated_at to prevent overwriting concurrent edits). For publishers managing large Ghost sites combining membership analytics, newsletter performance, and cross-team content workflows, RapidDev's team can help architect the complete Retool management solution.
1// createPost request body2{3 "posts": [{4 "title": "{{ input_postTitle.value }}",5 "status": "draft",6 "excerpt": "{{ input_excerpt.value }}",7 "featured": {{ toggle_featured.value || false }},8 "tags": {{ JSON.stringify(9 (input_tags.value || '').split(',').map(t => ({ name: t.trim() })).filter(t => t.name)10 ) }},11 "authors": [{ "id": "{{ select_author.value }}" }],12 "custom_excerpt": "{{ input_excerpt.value }}"13 }]14}Pro tip: Ghost's PUT /posts/{id} requires the current updated_at timestamp in the request body to prevent overwriting concurrent edits. Always fetch the post first and include its updated_at value in the update body, otherwise Ghost returns a 409 Conflict error.
Expected result: Newsletter analytics show per-newsletter open rates and subscriber counts. Post creation form creates draft posts in Ghost that editors can open and complete in Ghost's editor.
Common use cases
Build a Ghost content management and editorial dashboard
Create a Retool panel showing all Ghost posts (drafts, scheduled, and published) across all authors. Editors can filter by author, tag, and status, bulk-add tags to multiple posts, and update post metadata like excerpt and feature image URL — all faster than navigating Ghost's individual post editor.
Build a Retool Ghost content management panel. Show all posts with: title, author, status (draft/published/scheduled), publish date, tags, and featured image thumbnail. Add filters by author and status. Select multiple posts and add a tag to all of them with a Bulk Tag button. When a post is selected, show its excerpt and a link to edit it in Ghost. Include a summary row showing post count by status.
Copy this prompt to try it in Retool
Build a membership retention and churn analysis dashboard
Build a Retool panel combining Ghost member data with engagement metrics to identify at-risk subscribers. Show members who are paid (Stripe billing active) but have not opened any newsletter emails in the past 30 days, have not logged into Ghost in 60 days, or whose free trial is ending in the next 7 days. Retention teams can reach out proactively.
Build a Retool membership retention dashboard. Fetch Ghost members with their: email, tier (free/paid), subscription status, last login date, open rate, and created date. Filter to: active paid members who haven't opened an email in 30+ days. Sort by days since last open descending. Add a Send Re-engagement Email button for selected members. Show total at-risk member count and MRR at risk.
Copy this prompt to try it in Retool
Build a newsletter subscriber growth and engagement report
Create a Retool tool showing Ghost newsletter subscriber growth over time, segmented by tier (free vs. paid) and acquisition source. Include engagement metrics: average open rate, click rate, and churn rate. Marketing teams can track which acquisition channels produce the most engaged subscribers.
Build a Retool Ghost newsletter analytics panel. Show monthly subscriber counts for the last 12 months broken down by free vs. paid. Add summary stats: total members, paid conversion rate (paid/total %), average open rate, and monthly churn rate. Include a Chart with two series: new subscribers and churned subscribers by month to show net growth trend.
Copy this prompt to try it in Retool
Troubleshooting
401 Unauthorized on all Ghost Admin API requests
Cause: The Ghost Admin API JWT token has expired (tokens are short-lived, typically 5 minutes) or was not formatted correctly. Ghost's Admin API requires a properly signed JWT — not just the API key string itself.
Solution: Regenerate the JWT token. For manual setup, use jwt.io with: algorithm HS256, header { alg: 'HS256', kid: 'YOUR_KEY_ID', typ: 'JWT' }, payload { iat: current_unix_timestamp, exp: current + 300, aud: '/v3/admin/' }, and your hex-decoded key secret as the signature secret. Store the fresh token in GHOST_ADMIN_TOKEN configuration variable. For production, implement automated token refresh in a Retool Workflow.
1// Ghost JWT payload structure:2// {3// "iat": Math.floor(Date.now() / 1000),4// "exp": Math.floor(Date.now() / 1000) + 300,5// "aud": "/v3/admin/"6// }7// Sign with HS256, key = hex.decode(GHOST_ADMIN_KEY_SECRET)8// Header: { alg: 'HS256', kid: 'GHOST_ADMIN_KEY_ID', typ: 'JWT' }PUT /posts/{id} returns 409 Conflict when updating a post
Cause: Ghost uses optimistic locking for posts. The PUT request must include the current updated_at timestamp of the post. If this field is missing or outdated (another edit happened since you fetched the post), Ghost rejects the update.
Solution: Always fetch the post immediately before updating it with GET /posts/{id}. Include the updated_at field from that fresh response in your PUT request body: { 'posts': [{ 'title': '...', 'updated_at': '2024-01-01T00:00:00.000Z' }] }. Build your update flow so the fetch-then-update pattern is atomic in your Retool query chain.
1// PUT /posts/{id} body must include current updated_at:2{3 "posts": [{4 "title": "{{ input_title.value }}",5 "updated_at": "{{ getPostDetail.data.posts[0].updated_at }}"6 }]7}Members API returns 404 or empty results on Ghost Pro (hosted) instances
Cause: Ghost Pro's Admin API URL structure may differ from self-hosted Ghost. Also, Ghost Pro may require a specific API version in the URL path.
Solution: For Ghost Pro, verify your site URL is the Ghost.io subdomain: https://yoursite.ghost.io. The Admin API path for Ghost Pro is /ghost/api/admin/ (without version in path, using Accept-Version header instead). Try adding Accept-Version: v5.0 as a request header and updating the base URL to https://yoursite.ghost.io/ghost/api/admin.
Ghost filter query with multiple conditions returns no results
Cause: Ghost filter syntax uses + for AND and , for OR — not standard URL query operators. Incorrect syntax silently returns empty results instead of an error.
Solution: Verify Ghost filter syntax: use + for AND (status:published+featured:true), use , for OR (status:[draft,scheduled]), and use - for NOT (status:-draft). Encode + as %2B in URL parameters to prevent it being interpreted as a space. Test filters incrementally — start with one condition and add more until you find which one breaks the filter.
1// Ghost filter syntax examples:2// AND: status:published+tag:newsletter3// OR: status:[draft,scheduled]4// NOT: featured:-false5// Date: published_at:>2024-01-016// Encoded in URL: ?filter=status:published%2Bfeatured:trueBest practices
- Implement automated Ghost Admin API JWT token refresh in a Retool Workflow — tokens are short-lived (5 minutes) and manual refresh is impractical for an active dashboard. Refresh every 4 minutes on a schedule.
- Store Ghost Admin API key ID and secret separately as Retool Configuration Variables — the full key string format (id:secret) should be split so the secret can be marked as a secret config var and excluded from the frontend context.
- Always use the include parameter (include: tags,authors for posts; include: tiers,newsletters for members) to embed related objects in a single request rather than making separate lookup calls.
- For POST/PUT operations on Ghost posts, always fetch the current post state first and include its updated_at timestamp in the update body — Ghost's optimistic locking rejects updates that omit this field.
- Use Ghost's filter syntax for server-side filtering rather than fetching all records and filtering client-side — Ghost's API is not paginated across the full dataset and filtering server-side ensures you see results from the full content library.
- For membership tier filtering, use Ghost's status field (free, paid, comped) in the filter parameter rather than filtering on tier names, which are variable per publication.
- Cache the GET /tags/ and GET /authors/ queries for 10 minutes — these reference datasets change rarely and are often used to populate dropdown filters, making caching very effective.
- When building bulk post operations (bulk tagging, bulk status update), include a confirmation Modal before executing — bulk writes to Ghost cannot be undone easily and may affect published content visible to readers.
Alternatives
WordPress has a broader plugin ecosystem and supports Application Password auth without JWT — choose it over Ghost if your team needs extensive customization or already has WordPress expertise, while Ghost is better for publications focused on clean writing and native membership.
Notion functions as a CMS when paired with a frontend renderer — choose it over Ghost if your content lives in Notion databases already and you need to sync or display that content rather than manage a dedicated publishing platform.
Confluence is built for internal documentation and team knowledge bases — choose it over Ghost if your content is for internal audiences rather than public readers or paid subscribers.
Frequently asked questions
Does Retool have a native Ghost connector?
No, Retool does not have a dedicated native connector for Ghost. You connect via a REST API Resource using Ghost's Admin API JWT authentication. The setup takes about 15 minutes for initial configuration, plus additional time to implement automated JWT token refresh. Once configured, you have access to all Ghost Admin API endpoints including posts, pages, members, tiers, tags, and newsletter analytics.
What is the difference between Ghost's Admin API and Content API?
The Content API (public, read-only) provides access to published posts, pages, tags, and authors using a simple API key in a URL parameter — no JWT required. The Admin API (authenticated) provides full read/write access including drafts, unpublished content, member data, and all management operations. Use the Content API for public-facing Retool tools that only need to display published content. Use the Admin API for management panels that need to create, update, or access member data.
Can I create posts in Ghost from Retool without the full Lexical editor content?
Yes. Creating a post via POST /posts/ with only title, status: 'draft', and basic metadata fields creates an empty draft post that editors can then open in Ghost's native editor to write the content. This is useful for editorial workflows where Retool handles metadata management (tags, authors, scheduling) and Ghost handles the actual writing experience.
How do I access Ghost member email addresses from the API?
Ghost member email addresses are returned by the Admin API GET /members/ endpoint without any additional permissions — they are included by default in the member response object. Member data including email is only accessible via the Admin API (not the public Content API), so this requires JWT authentication with an Admin API key that has the appropriate permissions.
Can Retool trigger Ghost email newsletter sends?
Yes, indirectly. Ghost sends newsletters automatically when a post is published with its newsletter setting enabled. To trigger a newsletter send from Retool, update a post's status from 'draft' to 'published' using PUT /posts/{id} and include 'newsletter': { 'id': 'your_newsletter_id' } in the update body. This publishes the post and queues the newsletter email. Preview email sends (to admins only) can be triggered via POST /posts/{id}/email/send-test.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation