Skip to main content
StormyCMS

API reference

Fields and arguments use camelCase, such as createPage, layoutId, and pageBySlug. GraphQL type names are case-sensitive.

Endpoint

https://api.stormycms.com/graphql

An interactive GraphiQL explorer is served at the same path via GET.

Authentication

POST JSON to the endpoint with these headers:

HeaderValue
Content-Typeapplication/json
x-client-idSite client ID
x-client-secretSite 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 childComponents to 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 is admin or contributor
  • resendInvite(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

StormyCMS

The headless CMS where you own the editor.