Skip to main content
StormyCMS

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/graphql

An 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:

HeaderValueRequired
x-client-idYour site's client IDYes
x-client-secretYour site's client secretYes
Content-Typeapplication/jsonYes

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:

HeaderValue
AuthorizationBearer <jwt>

The JWT flow:

  1. The user signs in with GitHub or Google through the Stormy auth service; your app receives an auth code.
  2. Exchange it: exchangeAuthCode(code, clientId) → returns a sessionToken (stored as the stormy_session_token cookie by the boilerplate).
  3. 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 childComponents to 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 JWT
  • user(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!): Media
  • mediaItems(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!): Media
  • updateMedia(id: ID!, type: String, url: String, name: String, metadata: Json): Media
  • deleteMedia(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. GqlCreateSite has name, clientId, redirectUrls, adminIds, contributorIds
  • updateSite(id: ID!, name: String, ownerId: ID, adminIds: [ID], contributorIds: [ID], clientId: String, redirectUrls: [String]): Site
  • deleteSite(id: ID!): Site
  • resetClientId(siteId: ID!): Site
  • resetClientSecret(siteId: ID!): ApiKey
  • inviteUser(siteId: ID!, name: String!, email: String!, role: String!): Site — role must be admin or contributor; the invitee receives an email invite
  • resendInvite(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): User
  • removeOauthProvider(id: ID!, provider: String!): User
  • deleteUser(id: ID!): User

API Key Mutations

Require a user JWT.

  • createApiKey(input: GqlCreateApiKey!): ApiKeyWithToken — returns the plaintext key once
  • updateApiKey(id: ID!, input: GqlUpdateApiKey!): ApiKey
  • revokeApiKey(id: ID!): ApiKey
  • deleteApiKey(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) and GqlCreateSite / GqlCreateApiKey / GqlUpdateApiKey for 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

Last updated: 7/9/26, 6:42 AM

StormyCMSThe headless CMS where you own the management panel
Community
StormyCMS