API reference
Fields and arguments use camelCase, such as createPage, layoutId, and pageBySlug. GraphQL type names are case-sensitive.
Endpoint
https://api.stormycms.com/graphqlAn interactive GraphiQL explorer is served at the same path via GET.
Authentication
POST JSON to the endpoint with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
x-client-id | Site client ID |
x-client-secret | Site client secret |
Page, layout, and media reads use site credentials. Page and layout writes also require an authorized editor's Authorization: Bearer <jwt>. Media record writes use site credentials.
User and account operations require a user JWT with access to the requested resource.
The OAuth flow exchanges an auth code through exchangeAuthCode, stores the returned session token, and calls mintJwt for a short-lived JWT. Next.js auth handlers manage the callback and session cookie.
Keep site secrets and session tokens server-side. In the core client, pass a cookie header to session helpers or pass a JWT to write methods.
Rate limits
The default token bucket holds 10 tokens and refills at 10 per minute. JWT minting is limited per session. A rate-limit response includes a retry delay.
Queries
Page queries
page(id: ID!): page!
Returns a page in the authenticated site. A page in another site returns not-found.
Select
childComponentsto the depth you need. The client includes three child levels below each root component.
pages(limit: Int, offset: Int): [page!]!
Paginated list of the site's pages. limit defaults to 20, offset to 0.
pageBySlug(slug: String!): page
Returns a page by slug, or null when no page matches.
Layout queries
layout(id: ID!): layout!
layouts: [layout!]!
All layouts for the site.
Site queries
getCurrentSite: site!
Returns the site identified by your client credentials.
getSite(id: ID!): site!
Requires the site's API key or a site member's JWT.
getSites: [site!]!
Sites the authenticated user owns, administers, or contributes to. Requires a user JWT.
getSiteWithUsers(id: ID!): SiteWithUsers!
Site and resolved member records. Requires an owner or admin JWT.
User queries
Require a user JWT.
getUser: user!returns the current user.user(id: ID!): user!returns a user by ID.
Media queries
Require site credentials.
media(id: ID!): media!mediaItems(limit: Int, offset: Int): [media!]!
API key queries
Require a user JWT.
myApiKeys: [GqlApiKey!]!siteApiKeys(siteId: ID!): [GqlApiKey!]!apiKey(id: ID!): GqlApiKey!
Mutations
Page mutations
Require site credentials and an authorized editor JWT.
createPage(slug: String!, metadata: metadataInput!, components: [componentInput!]!, layoutId: ID!, createdAt: String!, updatedAt: String!): page!
updatePage(id: ID!, slug: String!, metadata: metadataInput!, components: [componentInput!]!, layoutId: ID!, updatedAt: String!): page!
Updates replace the stored component tree.
deletePage(id: ID!): page!
Layout mutations
Require site credentials and an authorized editor 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
Require site credentials. These operations change records, not stored files. See Media storage for uploads.
createMedia(type: String!, url: String!, name: String!, metadata: JSON!): media!updateMedia(id: ID!, type: String, url: String, name: String, metadata: JSON): media!deleteMedia(id: ID!): media!
Site and team mutations
Require a user JWT and the relevant site role. The account portal provides these controls.
addSite(input: GqlCreateSite!): site!Creates a site for the authenticated user.updateSite(id: ID!, name: String, ownerId: ID, adminIds: [ID!], contributorIds: [ID!], clientId: String, redirectUrls: [String!], adminPortalUrl: String, publicSiteUrl: String): site!deleteSite(id: ID!): site!resetClientId(siteId: ID!): site!resetClientSecret(siteId: ID!): GqlApiKeyWithToken!inviteUser(siteId: ID!, name: String!, email: String!, role: String!): site!Role isadminorcontributorresendInvite(siteId: ID!, oldInviteCode: String!): site!changeSiteMemberRole(siteId: ID!, userId: ID!, role: String!): site!removeSiteMember(siteId: ID!, userId: ID!): site!initiateOwnershipTransfer(siteId: ID!, email: String!): site!cancelOwnershipTransfer(siteId: ID!): site!acceptOwnershipTransfer(transferCode: String!): site!
Owners and admins can manage other members. They cannot change their own role or remove themselves. Ownership offers and cancellation require the owner; acceptance requires the recipient's session.
Auth mutations
exchangeAuthCode requires clientId for customer applications. mintJwt accepts a session token argument or an API session cookie.
exchangeAuthCode(code: String!, clientId: String): GqlAuthCodeExchange!mintJwt(sessionToken: String): GqlMintJwt!
User mutations
Require a user JWT. Accounts are created through OAuth sign-in.
updateUser(id: ID!, username: String, displayName: String, email: String, githubId: String, googleId: String): user!removeOauthProvider(id: ID!, provider: String!): user!deleteUser(id: ID!): user!
API key mutations
Require a user JWT.
createApiKey(input: GqlCreateApiKey!): GqlApiKeyWithToken!Returns the plaintext key once.updateApiKey(id: ID!, input: GqlUpdateApiKey!): GqlApiKey!revokeApiKey(id: ID!): GqlApiKey!deleteApiKey(id: ID!): GqlApiKey!
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 childLayout: layout 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!]!}Props and attrs
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!]! pendingOwnershipTransfer: GqlOwnershipTransfer clientId: String! redirectUrls: [String!]! adminPortalUrl: String! publicSiteUrl: String! monthlyApiRequests: Int! lastApiCall: String hasClientSecret: Boolean! createdAt: String! isAdmin(userId: ID!): Boolean! isContributor(userId: ID!): Boolean!}
type GqlOwnershipTransfer { toEmail: String! initiatedBy: ID! createdAt: String! expiresAt: String!}
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 accounts: [oauthAccount!]!}API keys
type GqlApiKey { 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 GqlApiKeyWithToken { 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!]!}Auth and team responses
type GqlAuthCodeExchange { sessionToken: String! user: user!}
type GqlMintJwt { jwt: String! expiresIn: Int!}
type oauthAccount { provider: String! providerId: String!}
type SiteWithUsers { site: site! users: SiteUsers!}
type SiteUsers { owner: user admins: [user!]! contributors: [user!]!}Input types
input metadataInput { title: String description: String keywords: [String!]}
input componentInput { id: String # Generated if not provided name: String! attrs: [attrInput!] props: [propInput!] childComponents: [componentInput!] parentComponentId: String}
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!]}Errors
GraphQL failures appear in errors, including on HTTP 200 responses:
{ "errors": [ { "message": "Not found", "path": ["page"] } ], "data": null}- Unauthorized. Check site credentials, session expiry, and the user's role.
- Not found. Check the resource ID and site.
- Rate limited. Wait for the retry delay before sending another request.
For client methods, see @stormycms/core.
Last updated: 10/20/26, 1:09 AM