Reference
GraphQL
Read pages, lists, menus, globals and campaigns in one request, with the schema, examples, errors and limits.
Reference11 min read
GraphQL reads the same content as the Delivery API, with the same keys. Use it when one page needs several things at once, such as the page, its menu and its banners, in a single request.
https://api.bifrostorbit.com/cms/graphqlhttps://api-stage.bifrostorbit.com/cms/graphql| Address | |
|---|---|
| Production | https://api.bifrostorbit.com/cms/graphql |
| Staging | https://api-stage.bifrostorbit.com/cms/graphql |
Try queries in Configuration › API playground. It opens on GraphQL, lists every query and type beside the editor, and shows the curl command for what you run.
Requests
Send a delivery key as Authorization: Bearer <key> or x-api-key: <key>, and POST a JSON body:
queryvariablesoperationName| Property | Type | Means |
|---|---|---|
query | string | The GraphQL document. Required. |
variables | object | Values for the query's variables |
operationName | string | Which operation to run when the document has several |
curl 'https://api.bifrostorbit.com/cms/graphql' \ -H 'Authorization: Bearer cms_pk_…' \ -H 'Content-Type: application/json' \ -d '{"query":"query ($url: String!) { page(url: $url, region: \"de\") { name url } }","variables":{"url":"/products/hiking-boots"}}'GET works too, with query, variables (as JSON text) and operationName in the query string. URL-encode them:
curl -G 'https://api.bifrostorbit.com/cms/graphql' \ -H 'Authorization: Bearer cms_pk_…' \ --data-urlencode 'query={ regions { key name } }'Answers are JSON with data, and errors when something went wrong:
{ "data": { "page": { "name": "Hiking boots", "url": "/products/hiking-boots" } } }Browser code on any website may call it with a public key; a secret key only works from a server. See From a browser in the Delivery API.
Queries
Every query that reads content takes region and locale, which work as in the Delivery API: a region key or code such as germany or de, and a language code such as de-DE. Leave them out for the defaults. draft: true reads drafts and needs a secret key.
pageurl: String!, locale, region, draft, strict: Boolean, references: BooleanPage. One page by address, with language and region fallback unless strict is true.pagestype: String!, locale, region, draft, page: Int, perPage: Int, references: BooleanPageList!. Entries of one content type, in the order editors set. page defaults to 1, perPage to 20, at most 100.globalkey: String!, locale, region, draft, references: BooleanGlobal. A global entry such as the footer, with language fallback.navigationkey: String!, locale, region, draftNavigation. A menu.regions[Region!]!. The regions and languages the key may read.resolveurl: String!, locale, region, draft, references: BooleanResolution!. Whether an address is a page, a redirect, gone or not found.sitemapregion, locale, page: IntJSON!. Without region, the index of every region's sitemap; with it, that sitemap's entries.bannersregion, locale, path: String, everyPage: Boolean, draftJSON!. Banners live now for a page, pinned first, with serverTime. everyPage: true returns those for every page instead.popupsregion, locale, path: String, loggedIn: Boolean, draftJSON!. Popups live now for a page and sign-in state, highest priority first, with serverTime.| Query | Arguments | Returns |
|---|---|---|
page | url: String!, locale, region, draft, strict: Boolean, references: Boolean | Page. One page by address, with language and region fallback unless strict is true. |
pages | type: String!, locale, region, draft, page: Int, perPage: Int, references: Boolean | PageList!. Entries of one content type, in the order editors set. page defaults to 1, perPage to 20, at most 100. |
global | key: String!, locale, region, draft, references: Boolean | Global. A global entry such as the footer, with language fallback. |
navigation | key: String!, locale, region, draft | Navigation. A menu. |
regions | [Region!]!. The regions and languages the key may read. | |
resolve | url: String!, locale, region, draft, references: Boolean | Resolution!. Whether an address is a page, a redirect, gone or not found. |
sitemap | region, locale, page: Int | JSON!. Without region, the index of every region's sitemap; with it, that sitemap's entries. |
banners | region, locale, path: String, everyPage: Boolean, draft | JSON!. Banners live now for a page, pinned first, with serverTime. everyPage: true returns those for every page instead. |
popups | region, locale, path: String, loggedIn: Boolean, draft | JSON!. Popups live now for a page and sign-in state, highest priority first, with serverTime. |
locale and region are String, and draft is Boolean, everywhere. references: true replaces Reference and References values in fields and components with the entries they point at, as include=references does in REST. See References in the Delivery API. sitemap, banners and popups answer with the same JSON as their REST endpoints, under serverTime and items for campaigns.
Some options exist only in REST: sorting, filtering, tag, category and q for lists, strict for globals and addresses, and loggedIn for banners. Use the Delivery API for those.
Types
JSON holds content shaped by your space, such as fields and components, and cannot be selected into. DateTime is an ISO 8601 timestamp such as 2026-10-01T09:30:00.000Z.
Page
urlString!nameString!contentTypeString!locale, regionString!requestedLocale, requestedRegionString!fallbackAppliedBoolean!regionFallbackAppliedBoolean!draftBoolean!publishedAtDateTimenullversionInt!fieldsJSON!componentsJSON!childrenseoJSON!| Field | Type | Means |
|---|---|---|
url | String! | The page address |
name | String! | Its name in the CMS |
contentType | String! | Content type key |
locale, region | String! | The language and region that answered |
requestedLocale, requestedRegion | String! | What was asked for |
fallbackApplied | Boolean! | Another language answered |
regionFallbackApplied | Boolean! | Another region answered |
draft | Boolean! | This is a draft |
publishedAt | DateTime | Last published, or null |
version | Int! | Published version, or draft version for a draft |
fields | JSON! | The content type's fields, with images and links filled in |
components | JSON! | The page body: the component tree to render. Blocks in a slot are on the slot's field as well as in children |
seo | JSON! | Title, description, canonical, robots, Open Graph, X card, alternates, breadcrumbs and JSON-LD |
PageList and Summary
PageListitems[Summary!]!totalInt!, matching entries across all pagespageInt!hasMoreBoolean!SummaryidString!, the entry's id, the value a Reference field holdsurlString, null for entries without an addressname, contentTypeString!publishedAtDateTimeseoJSON, null for entries without an addressfieldsJSON, the entry's fields, read only when you select it| Type | Field | Type |
|---|---|---|
PageList | items | [Summary!]! |
total | Int!, matching entries across all pages | |
page | Int! | |
hasMore | Boolean! | |
Summary | id | String!, the entry's id, the value a Reference field holds |
url | String, null for entries without an address | |
name, contentType | String! | |
publishedAt | DateTime | |
seo | JSON, null for entries without an address | |
fields | JSON, the entry's fields, read only when you select it |
Summaries leave the page body out on purpose. Ask page for one. Select fields to read entries without an address, such as FAQ entries, in one request.
Global
key, name, contentTypeString!localeString!, the language that answeredfields, componentsJSON!| Field | Type |
|---|---|
key, name, contentType | String! |
locale | String!, the language that answered |
fields, components | JSON! |
Navigation and NavItem
Navigationkey, nameString!items[NavItem!]!NavItemlabelString!href, target, icon, badgeStringchildren[NavItem!]!| Type | Field | Type |
|---|---|---|
Navigation | key, name | String! |
items | [NavItem!]! | |
NavItem | label | String! |
href, target, icon, badge | String | |
children | [NavItem!]! |
Select children again for each level of the menu you want.
Region and Locale
Regionkey, nameString!locales[Locale!]!Localecode, nameString!isDefaultBoolean!| Type | Field | Type |
|---|---|---|
Region | key, name | String! |
locales | [Locale!]! | |
Locale | code, name | String! |
isDefault | Boolean! |
Resolution and Redirect
ResolutionkindString!: page, redirect, gone or notFoundpagePage, set when kind is pageredirectRedirect, set when kind is redirectRedirectdestinationString!statusCode, hopsInt!permanentBoolean!, true for 301 and 308| Type | Field | Type |
|---|---|---|
Resolution | kind | String!: page, redirect, gone or notFound |
page | Page, set when kind is page | |
redirect | Redirect, set when kind is redirect | |
Redirect | destination | String! |
statusCode, hops | Int! | |
permanent | Boolean!, true for 301 and 308 |
Unlike REST, resolve answers 200 for every kind. Read kind to decide what to do.
Examples
A page with its menus and banners
One request for everything a page template needs. Aliases fetch two menus at once.
query PageView($url: String!, $region: String) { page(url: $url, region: $region) { name url locale fallbackApplied components seo } header: navigation(key: "main", region: $region) { items { label href children { label href } } } footer: navigation(key: "footer", region: $region) { items { label href } } banners(path: $url, region: $region)}{ "url": "/products/hiking-boots", "region": "de" }A paged list
query Products($page: Int) { pages(type: "product", region: "de", page: $page, perPage: 12) { items { url name publishedAt } total page hasMore }}Ask for the next page while hasMore is true.
Entries without an address
FAQ entries, authors and other entries without a page of their own are read through pages with their fields:
{ pages(type: "faqEntry", region: "de", perPage: 50) { items { id name fields } }}A page with the entries it references
{ page(url: "/pricing", region: "de", references: true) { components }}A References field in a component then holds the entries, each with id, url, name, contentType, publishedAt and fields, instead of their ids.
A global entry
{ global(key: "footer", region: "de") { name locale fields components }}Before rendering an address
query Route($url: String!) { resolve(url: $url, region: "de") { kind redirect { destination statusCode } page { name components seo } }}Drafts and preview
Add draft: true to each query that should read drafts, and send a secret key (cms_sk_…). Secret keys exist only for staging environments. A public key asking for drafts gets 401.
query Preview($url: String!) { page(url: $url, region: "de", draft: true) { name version components }}From the SDK
const data = await cms.graphql<{ page: { name: string; components: unknown[] } }>( 'query ($url: String!) { page(url: $url, region: "de") { name components } }', { variables: { url: '/products/hiking-boots' } },)cms.graphql(query, options) answers with data. The options are variables, operationName, preview and signal. Pass the expected shape as the type.
preview: truesends the client's preview key instead of its public key. It does not adddraft: true; put that in the query.- GraphQL requests are never cached by the SDK and are not retried.
- Any error in the answer is thrown, not returned beside partial data:
CmsAuthErrorfor401,CmsRateLimitErrorfor429,CmsUnavailableErrorfor500or when the API cannot be reached, andCmsErrorwith the status and message for anything else, such as a missing page.
See the SDK.
Errors
Errors come in errors, each with a message, a code under extensions, and the path and locations of the field that failed when there is one:
{ "data": { "page": null }, "errors": [ { "message": "Content not found", "locations": [{ "line": 1, "column": 3 }], "path": ["page"], "extensions": { "code": "not_found" } } ]}400query, variables that are not a JSON object, a query that does not parse or does not match the schema (bad_request), or one over the limits (query_too_deep, query_too_complex). data is left out.401unauthorized)404402plan_ended)429"extensions": { "code": "rate_limited" } and the answer has the same Retry-After and X-RateLimit-… headers as the REST endpoints. Every other answer has the X-RateLimit-… headers too. See the Delivery API. data is left out.413query_too_large)500internal_error). The message is general and the details stay on our side. Try again.200| Status | When |
|---|---|
400 | No query, variables that are not a JSON object, a query that does not parse or does not match the schema (bad_request), or one over the limits (query_too_deep, query_too_complex). data is left out. |
401 | A missing, unknown, deleted or expired key, a public key asking for drafts, a secret key sent from a browser on another website, or a region or language the key may not read (unauthorized) |
404 | A page, global entry, menu, region or language that does not exist |
402 | The organisation's CMS Orbit plan has ended (plan_ended) |
429 | Too many requests. The error has "extensions": { "code": "rate_limited" } and the answer has the same Retry-After and X-RateLimit-… headers as the REST endpoints. Every other answer has the X-RateLimit-… headers too. See the Delivery API. data is left out. |
413 | A query longer than 20,000 characters (query_too_large) |
500 | Something failed on our side (internal_error). The message is general and the details stay on our side. Try again. |
200 | Success |
- When
page,globalornavigationfails, that field isnulland the other fields still answer, so a missing menu does not lose the page. pages,regions,resolve,sitemap,bannersandpopupsnever answernull, so when one of them fails the wholedataisnull.- When several fields fail, the HTTP status comes from the first error.
page,globalandnavigationreport a missing item as an error rather than a quietnull. Checkerrorsbefore usingdata.
Limits
- Each request counts once against the rate limit of 15,000 requests a minute per key and per IP address, shared with the REST endpoints. See the Delivery API.
perPageinpagesis at most 100.- A query may be at most 20,000 characters long.
- A query may nest at most 10 levels, counting from the top field, and ask for at most 300 fields. Each alias and each use of a fragment counts. Split a bigger read into several requests.
- Introspection is on, so GraphQL tools can read the schema from the address, and introspection fields do not count towards the limits. The API playground shows the schema too.
- Persisted queries are not supported. Send the full query each time.
Caching
Every GraphQL answer has Cache-Control: no-store and Vary: Authorization, so nothing between your website and the API keeps it. Cache in your website instead, and refresh on publish webhooks. See Refresh the website when content changes. For pages a CDN should cache, the REST endpoints carry cache headers.
Was this page helpful?