API Reference
Reference for the GraphQL operations, types, and inputs available in the StormyCMS API.
Field and argument names are camelCase (e.g. createPage, layoutId, pageBySlug).
Base URL
https://api.stormycms.com/graphqlAn interactive GraphiQL explorer is served at the same path via GET.
Authentication
There are two levels of access:
Site credentials (content reads)
Every request identifies your site with these headers:
| Header | Value | Required |
|---|---|---|
x-client-id | Your site's client ID | Yes |
x-client-secret | Your site's client secret | Yes |
Content-Type | application/json | Yes |
Site credentials are sufficient for content queries: page, pages, pageBySlug, layout, layouts, and getCurrentSite.
User JWT (editing and account operations)
Mutations that change content, and queries that touch user or account data, additionally require a short-lived JWT in the Authorization header:
| Header | Value |
|---|---|
Authorization | Bearer <jwt> |
The JWT flow:
- The user signs in with GitHub or Google through the Stormy auth service; your app receives an auth code.
- Exchange it:
exchangeAuthCode(code, clientId)→ returns asessionToken(stored as thestormy_session_tokencookie by the boilerplate). - Mint a JWT when needed:
mintJwt(sessionToken)→ returns{ jwt, expiresIn }.
If you use @stormycms/next, createNextAuthHandlers wires this entire flow into a single route handler, and @stormycms/core's StormyCMSClient mints JWTs automatically from the session cookie.
Rate Limits
The API applies token-bucket rate limiting per session/key. The default configuration is a 10-token bucket refilled at 10 tokens per minute. When limited, the response indicates how long to wait before retrying.
JWT minting (mintJwt) is additionally rate-limited to 10 requests per session per minute, so mint on demand rather than in a hot loop (the official client already does this).
Queries
Page Queries
page(id: ID!): Page
Retrieve a page by ID. Scoped to the authenticated site — pages from other sites return not-found.
query GetPage($id: ID!) { page(id: $id) { id name slug metadata { title description keywords } layoutId siteId createdAt updatedAt components { id order name attrs { name value } props { name value } parentComponentId childComponents { id name props { name value } } } }}Note: GraphQL cannot express unbounded recursion, so query
childComponentsto the depth you need (the official client expands three levels).
pages(limit: Int, offset: Int): [Page!]!
Paginated list of the site's pages. limit defaults to 20, offset to 0.
pageBySlug(slug: String!): Page
Fetch a page by its URL slug — the primary lookup used when rendering the public site. Returns null when no page matches.
Layout Queries
layout(id: ID!): Layout
Retrieve a layout by ID.
layouts: [Layout!]!
All layouts for the site.
Site Queries
All site queries require either your site credentials or a user JWT.
getCurrentSite: Site
The site identified by your client credentials. This is the usual way to read your own site record with a site API key.
getSite(id: ID!): Site
Get a site by ID. Requires the site's own API key, or a user JWT for the site owner.
getSites: [Site!]!
Sites owned by the authenticated user. Requires a user JWT.
getSiteWithUsers(id: ID!): SiteWithUsers
Site plus resolved user objects (owner, admins, contributors) and pending invites. Requires the site owner's JWT — the resolved emails are deliberately not exposed to site API keys.
User Queries
Require a user JWT.
getUser: User— the current user, derived from the JWTuser(id: ID!): User— a user by ID (visible to you)
Media Queries
Media is a site-scoped resource, so these queries authenticate with your site credentials (same as page/layout reads). They do not require a user JWT.
media(id: ID!): MediamediaItems(limit: Int, offset: Int): [Media!]!
API Key Queries
Require a user JWT.
myApiKeys: [ApiKey!]!siteApiKeys(siteId: ID!): [ApiKey!]!apiKey(id: ID!): ApiKey
Mutations
Page Mutations
Require a user JWT (plus site credential headers).
createPage(slug: String!, metadata: metadataInput!, components: [componentInput!]!, layoutId: ID!, createdAt: String!, updatedAt: String!): Page
mutation CreatePage( $slug: String! $metadata: metadataInput! $components: [componentInput!]! $layoutId: ID! $createdAt: String! $updatedAt: String!) { createPage( slug: $slug metadata: $metadata components: $components layoutId: $layoutId createdAt: $createdAt updatedAt: $updatedAt ) { id slug metadata { title description keywords } }}updatePage(id: ID!, slug: String!, metadata: metadataInput!, components: [componentInput!]!, layoutId: ID!, updatedAt: String!): Page
deletePage(id: ID!): Page
Layout Mutations
Require a user JWT.
createLayout(name: String!, components: [componentInput!]!, createdAt: String!, updatedAt: String!): Layout
updateLayout(id: ID!, name: String!, components: [componentInput!]!, parentLayoutId: ID, updatedAt: String!): Layout
deleteLayout(id: ID!): Layout
Media Mutations
Media records reference files you upload to your own S3-compatible storage. These mutations require your site credentials (the same x-client-id / x-client-secret headers used for content reads). The official Next.js admin client also sends the editor JWT, but the API authorizes the request via the site API key.
createMedia(type: String!, url: String!, name: String!, metadata: Json!): MediaupdateMedia(id: ID!, type: String, url: String, name: String, metadata: Json): MediadeleteMedia(id: ID!): Media
Site & Team Mutations
Require a user JWT. Most teams manage these through the account portal at account.stormycms.com rather than calling them directly.
addSite(input: GqlCreateSite!): Site— create a site for yourself.GqlCreateSitehasname,clientId,redirectUrls,adminIds,contributorIdsupdateSite(id: ID!, name: String, ownerId: ID, adminIds: [ID], contributorIds: [ID], clientId: String, redirectUrls: [String]): SitedeleteSite(id: ID!): SiteresetClientId(siteId: ID!): SiteresetClientSecret(siteId: ID!): ApiKeyinviteUser(siteId: ID!, name: String!, email: String!, role: String!): Site— role must beadminorcontributor; the invitee receives an email inviteresendInvite(siteId: ID!, oldInviteCode: String!): Site
Auth Mutations
exchangeAuthCode(code: String!, clientId: String!): { sessionToken, user }mintJwt(sessionToken: String!): { jwt, expiresIn }
User Mutations
Require a user JWT; operations apply to your own account. (Accounts are created by signing in with GitHub or Google — there is no manual user-creation mutation to call.)
updateUser(id: ID!, username: String, displayName: String, email: String, githubId: String, googleId: String): UserremoveOauthProvider(id: ID!, provider: String!): UserdeleteUser(id: ID!): User
API Key Mutations
Require a user JWT.
createApiKey(input: GqlCreateApiKey!): ApiKeyWithToken— returns the plaintext key onceupdateApiKey(id: ID!, input: GqlUpdateApiKey!): ApiKeyrevokeApiKey(id: ID!): ApiKeydeleteApiKey(id: ID!): ApiKey
Types
Page
type Page { id: ID! name: String slug: String metadata: Metadata! layoutId: ID siteId: ID! createdAt: String updatedAt: String components: [Component!]! layouts: [Layout!]! # All layouts for the site, for building the layout chain}Layout
type Layout { id: ID! name: String! isDefault: Boolean! outletId: ID # Component where page content renders childLayoutId: ID parentLayoutId: ID componentIds: [ID!]! attrs: [Attr!]! props: [Prop!]! components: [Component!]! createdAt: String updatedAt: String}Component
type Component { id: ID! order: Int name: String! # Matches an export in your CMS export map attrs: [Attr!]! props: [Prop!]! parentComponentId: ID childComponents: [Component!]!}Metadata
type Metadata { title: String! description: String! keywords: [String!]!}Prop / Attr
type Prop { name: String! value: Json! # Arbitrary Json value}
type Attr { name: String! value: String!}Site
type Site { id: ID! name: String! ownerId: ID! adminIds: [UserIdWithDate!]! contributorIds: [UserIdWithDate!]! pendingUsers: [PendingUser!]! clientId: String! redirectUrls: [String!]! createdAt: String! isAdmin(userId: ID!): Boolean! isContributor(userId: ID!): Boolean!}
type UserIdWithDate { userId: ID! addDate: String!}
type PendingUser { name: String! email: String! role: String! inviteCode: String! createdAt: String!}Media
type Media { id: ID! type: String! url: String! name: String! metadata: Json! siteId: ID! createdAt: String! updatedAt: String!}User
type User { id: ID! username: String displayName: String email: String githubId: String googleId: String}ApiKey / ApiKeyWithToken
type ApiKey { id: ID! name: String! permissions: [String!]! isActive: Boolean! createdBy: ID! siteId: ID! createdAt: String! expiresAt: String lastUsed: String isPublic: Boolean! prefix: String scopes: [String!]!}
type ApiKeyWithToken { id: ID! name: String! key: String! # Plaintext; only returned at creation permissions: [String!]! isActive: Boolean! createdBy: ID! siteId: ID! createdAt: String! expiresAt: String isPublic: Boolean! prefix: String scopes: [String!]!}Input Types
input metadataInput { title: String description: String keywords: [String]}
input componentInput { id: ID # Generated if not provided name: String! order: Int attrs: [attrInput] props: [propInput] childComponents: [componentInput] parentComponentId: ID}
input propInput { name: String! value: Json!}
input attrInput { name: String! value: String!}
input GqlCreateSite { name: String! clientId: String redirectUrls: [String] adminIds: [ID] contributorIds: [ID]}
input GqlCreateApiKey { name: String! permissions: [String!]! expiresInDays: Int siteId: ID! isPublic: Boolean scopes: [String!]}
input GqlUpdateApiKey { name: String permissions: [String!] isActive: Boolean siteId: ID! expiresAt: String isPublic: Boolean scopes: [String!]}Input type names: The server exposes these as lower-camelCase (
metadataInput,componentInput,attrInput,propInput) andGqlCreateSite/GqlCreateApiKey/GqlUpdateApiKeyfor the site/API-key inputs. The names above match the live GraphQL schema.
Error Handling
Errors are returned in the standard GraphQL errors array:
{ "errors": [ { "message": "Not found", "path": ["page"] } ], "data": null}Common failure modes:
- Unauthorized — missing/invalid client credentials or expired JWT; re-mint the JWT via
mintJwt - Not found — the resource doesn't exist or belongs to another site (existence is intentionally not revealed across sites)
- Rate limited — back off and retry after the indicated delay
Using the Typed Client
Instead of raw fetch calls, use StormyCMSClient from @stormycms/core:
import { StormyCMSClient } from '@stormycms/core';
const client = new StormyCMSClient(); // Reads STORMY_CMS_CLIENT_ID / STORMY_CMS_CLIENT_SECRET from env
const page = await client.getPageBySlug({ slug: 'home' });const layouts = await client.getLayouts();Next Steps
- Getting Started - Scaffold a project
- GraphQL Queries - Query walkthroughs
- GraphQL Mutations - Mutation walkthroughs
- Examples - Practical implementations
Last updated: 7/9/26, 6:42 AM