Summary and recommendation
Ghost's Admin API provides full read/write access to staff users under a single Admin API Key - there is no granular scope system; each key is all-or-nothing. Authentication uses a short-lived JWT (max 5-minute expiry) signed with HMAC-SHA256 using the hex-decoded secret from the key pair.
All request and response bodies wrap resources in a named array (e.g., `{"users":[...]}`) even for single-resource operations, which is a consistent but non-standard envelope pattern.
The API exposes endpoints for listing, reading, updating, and deleting staff users, plus a separate `/invites/` resource for provisioning. Staff users cannot be created directly - the only creation path is `POST /ghost/api/admin/invites/`, which triggers an email the recipient must manually accept before a user record is created.
This human-in-the-loop requirement is a hard constraint for any fully automated provisioning pipeline.
Ghost has no native SCIM 2.0 or SSO support on any plan. Integrating Ghost into an identity graph requires building against the Admin API directly, using webhook events (`user.added`, `user.edited`, `user.deleted`) for near-real-time sync, or falling back to scheduled polling of `GET /users/`.
Stitchflow's MCP server with 60+ deep IT/identity integrations can abstract this complexity, but the invite-acceptance gap remains a platform-level constraint that no integration layer can fully automate.
API quick reference
| Has user API | Yes |
| Auth method | JWT (Admin API Key) — a key ID and secret are combined to sign a short-lived JWT Bearer token; no OAuth 2.0 flow. |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | N/A |
Authentication
Auth method: JWT (Admin API Key) - a key ID and secret are combined to sign a short-lived JWT Bearer token; no OAuth 2.0 flow.
Setup steps
- In Ghost Admin, navigate to Settings → Integrations → Add custom integration.
- Copy the Admin API Key (format:
{key_id}:{secret}). - Split the key on
:to obtainkey_idandsecret. - Sign a JWT: header
{alg:'HS256',typ:'JWT',kid:key_id}, payload{iat:<now_unix>,exp:<now+5min>,aud:'/admin/'}, signed withBuffer.from(secret,'hex')using HMAC-SHA256. - Send requests with header
Authorization: Ghost <jwt_token>.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| Admin API Key (full access) | Admin API keys grant full read/write access to all Admin API resources including staff users. There is no granular scope system; access is all-or-nothing per key. | All user-management operations (list, read, invite, update, delete staff users) |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string | Unique internal Ghost user ID. | auto-generated | immutable | Used in URL path for single-user operations. |
| name | string | Staff member's display name. | required | optional | |
| slug | string | URL-safe identifier derived from name. | auto-generated | optional | Must be unique across users. |
| string | Staff member's email address; used for login. | required (via invite) | optional | Must be unique. New staff are created via invite endpoint, not direct POST. | |
| profile_image | string (URL) | URL to the user's avatar image. | optional | optional | |
| cover_image | string (URL) | URL to the user's cover/banner image. | optional | optional | |
| bio | string | Short biography displayed on author pages. | optional | optional | |
| website | string (URL) | User's personal website URL. | optional | optional | |
| location | string | User's location string. | optional | optional | |
| string | Facebook profile URL or username. | optional | optional | ||
| string | Twitter/X handle. | optional | optional | ||
| accessibility | string (JSON) | Serialized UI accessibility/preference settings. | optional | optional | Internal Ghost Admin UI preferences. |
| status | string (enum) | User account status: active, inactive. |
auto-set to active on acceptance |
optional | Setting to inactive suspends login access. |
| meta_title | string | SEO meta title for the author page. | optional | optional | |
| meta_description | string | SEO meta description for the author page. | optional | optional | |
| tour | string (JSON) | Tracks which onboarding tour steps have been completed. | auto-generated | optional | |
| last_seen | string (ISO 8601) | Timestamp of last Admin UI login. | null | system-managed | Read-only in practice. |
| created_at | string (ISO 8601) | Timestamp when the user record was created. | auto-generated | immutable | |
| updated_at | string (ISO 8601) | Timestamp of last update. | auto-generated | auto-updated | |
| roles | array of role objects | Assigned roles: Owner, Administrator, Editor, Author, Contributor. | set via invite role field |
optional (PUT roles sub-resource) | Only one role per user. Owner role cannot be assigned via API. |
Core endpoints
List staff users
- Method: GET
- URL:
https://{site}/ghost/api/admin/users/?limit=all&include=roles - Watch out for: Default
limitis 15. Uselimit=allto retrieve all users in one request. Does NOT return Members (subscribers), only staff.
Request example
GET /ghost/api/admin/users/?limit=all&include=roles
Authorization: Ghost <jwt>
Response example
{
"users": [
{"id":"1","name":"Jane","email":"jane@example.com",
"status":"active","roles":[{"name":"Administrator"}]}
],
"meta":{"pagination":{"page":1,"limit":"all","total":1}}
}
Read single staff user by ID
- Method: GET
- URL:
https://{site}/ghost/api/admin/users/{id}/?include=roles - Watch out for: Response is always wrapped in a
usersarray even for single-resource reads.
Request example
GET /ghost/api/admin/users/6123abc/?include=roles
Authorization: Ghost <jwt>
Response example
{
"users": [
{"id":"6123abc","name":"Jane","email":"jane@example.com",
"status":"active","roles":[{"name":"Editor"}]}
]
}
Read current authenticated user (me)
- Method: GET
- URL:
https://{site}/ghost/api/admin/users/me/?include=roles - Watch out for: Admin API keys are associated with an integration pseudo-user, not a real staff account.
mereturns that integration user.
Request example
GET /ghost/api/admin/users/me/
Authorization: Ghost <jwt>
Response example
{
"users": [
{"id":"6123abc","name":"API Integration",
"roles":[{"name":"Administrator"}]}
]
}
Update staff user
- Method: PUT
- URL:
https://{site}/ghost/api/admin/users/{id}/ - Watch out for: Request body must wrap the user object in a
usersarray. Must includeidinside the body object. Omittingupdated_atmay cause a version conflict error on some Ghost versions.
Request example
PUT /ghost/api/admin/users/6123abc/
Authorization: Ghost <jwt>
Content-Type: application/json
{"users":[{"id":"6123abc","name":"Jane Doe","bio":"Editor"}]}
Response example
{
"users": [
{"id":"6123abc","name":"Jane Doe",
"bio":"Editor","updated_at":"2025-01-10T12:00:00.000Z"}
]
}
Invite new staff user
- Method: POST
- URL:
https://{site}/ghost/api/admin/invites/ - Watch out for: Staff users cannot be created directly via POST /users/. The only creation path is sending an invite. The invited user must accept via email link before a user record is created.
role_idmust be a valid UUID from GET /roles/.
Request example
POST /ghost/api/admin/invites/
Authorization: Ghost <jwt>
Content-Type: application/json
{"invites":[{"email":"new@example.com","role_id":"<role_uuid>"}]}
Response example
{
"invites": [
{"id":"inv_abc","email":"new@example.com",
"status":"sent","created_at":"2025-01-10T12:00:00.000Z"}
]
}
List pending invites
- Method: GET
- URL:
https://{site}/ghost/api/admin/invites/ - Watch out for: Invites expire after 7 days. Expired invites remain in the list with status
expireduntil deleted.
Request example
GET /ghost/api/admin/invites/
Authorization: Ghost <jwt>
Response example
{
"invites": [
{"id":"inv_abc","email":"new@example.com",
"status":"sent","expires":"2025-01-17T12:00:00.000Z"}
]
}
Delete staff user
- Method: DELETE
- URL:
https://{site}/ghost/api/admin/users/{id}/ - Watch out for: Deleting a user permanently removes them and reassigns their posts to the Owner. The Owner user cannot be deleted via API.
Request example
DELETE /ghost/api/admin/users/6123abc/
Authorization: Ghost <jwt>
Response example
HTTP 204 No Content
List roles
- Method: GET
- URL:
https://{site}/ghost/api/admin/roles/ - Watch out for: Role UUIDs are instance-specific and must be fetched dynamically; do not hardcode them across Ghost installations.
Request example
GET /ghost/api/admin/roles/
Authorization: Ghost <jwt>
Response example
{
"roles": [
{"id":"uuid-admin","name":"Administrator"},
{"id":"uuid-editor","name":"Editor"},
{"id":"uuid-author","name":"Author"}
]
}
Rate limits, pagination, and events
- Rate limits: Ghost does not publish explicit rate-limit tiers in official documentation. Self-hosted instances have no enforced API rate limits by default. Ghost Pro (managed hosting) may apply undocumented infrastructure-level throttling.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: No official rate-limit headers or documented thresholds found in Ghost Admin API docs as of 2025.
- Pagination method: offset
- Default page size: 15
- Max page size: 15
- Pagination pointer: page / limit
| Plan | Limit | Concurrent |
|---|---|---|
| Self-hosted (open source) | No enforced limit (server capacity dependent) | 0 |
| Ghost Pro (all plans) | Not publicly documented | 0 |
- Webhooks available: Yes
- Webhook notes: Ghost Admin API supports webhooks configurable via Settings → Integrations or via POST /webhooks/. Webhooks fire on content and member lifecycle events. Staff user lifecycle events (user.added, user.edited, user.deleted) are available.
- Alternative event strategy: Polling GET /users/ on a schedule if webhooks are not feasible.
- Webhook events: user.added, user.edited, user.deleted, member.added, member.edited, member.deleted, post.published, post.unpublished, post.added, post.edited, post.deleted, site.changed
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: N/A
- Endpoint: Not documented
Limitations:
- Ghost has no native SCIM 2.0 support on any plan.
- No native SSO (SAML/OIDC) support; third-party solutions (e.g., miniOrange) are required.
- Staff user provisioning must be done via the Admin API invite flow.
Common scenarios
Provision a new staff editor: fetch role UUIDs dynamically from GET /ghost/api/admin/roles/ - they are instance-specific and must never be hardcoded - then POST /ghost/api/admin/invites/ with the target email and role UUID.
Automate only to this point; the invitee must accept via email link before a user record exists in /users/. Invites expire after 7 days and remain in the list with status expired until explicitly deleted.
Suspend a staff user without deleting their content: PUT /ghost/api/admin/users/{id}/ with {"users":[{"id":"{id}","status":"inactive"}]}. This blocks login while preserving posts. The Owner account cannot be set to inactive via API.
Audit full staff access: GET /ghost/api/admin/users/?limit=all&include=roles retrieves all active staff (default page size is 15; limit=all is required for complete results). Follow with GET /ghost/api/admin/invites/ - pending invitees do not appear in /users/ until accepted, so both endpoints are required for a complete access picture.
Provision a new staff editor
- GET /ghost/api/admin/roles/ to retrieve the UUID for the 'Editor' role.
- POST /ghost/api/admin/invites/ with body
{"invites":[{"email":"editor@example.com","role_id":"<editor_uuid>"}]}. - The invitee receives an email; they must click the link and set a password to activate their account.
- After acceptance, GET /ghost/api/admin/users/?filter=email:'editor@example.com'&include=roles to confirm the user record exists with status
active.
Watch out for: There is no way to programmatically complete the invite acceptance step - it requires human interaction via email link. Automate only up to sending the invite.
Suspend (deactivate) a staff user
- GET /ghost/api/admin/users/?filter=email:'user@example.com' to retrieve the user's
id. - PUT /ghost/api/admin/users/{id}/ with body
{"users":[{"id":"{id}","status":"inactive"}]}. - Confirm response contains
"status":"inactive".
Watch out for: Setting status to inactive prevents login but does not delete the user or their content. The Owner account cannot be set to inactive via API.
Audit all staff users and their roles
- GET /ghost/api/admin/users/?limit=all&include=roles to retrieve all staff users with role data in a single request.
- GET /ghost/api/admin/invites/ to retrieve any pending invites not yet accepted.
- Combine both lists to produce a full picture of current and pending staff access.
Watch out for: Pending invitees do not appear in /users/ until they accept the invite. Always check /invites/ separately to audit who has been granted access but not yet activated.
Why building this yourself is a trap
The invite-only provisioning model is the primary automation trap: there is no POST /users/ endpoint, so any pipeline that assumes it can create a user record programmatically will fail silently or error. The human acceptance step is non-negotiable and must be documented in any runbook.
JWT generation is error-prone: the secret component of the Admin API Key is hex-encoded and must be decoded with Buffer.from(secret, 'hex') before use as the HMAC signing key. Passing the raw string produces a valid-looking but incorrect signature that Ghost will reject.
Tokens must also expire within 5 minutes; long-lived tokens are rejected at the API layer.
Ghost Pro does not publish rate-limit thresholds or expose rate-limit headers, so backoff logic cannot be tuned to documented limits - implement conservative exponential backoff by default.
On Ghost Pro, the me endpoint returns an integration pseudo-user, not a real staff account, which can produce misleading results if used for identity resolution in an identity graph context.
Automate Ghost workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.