# GraphQL

> Read pages, lists, menus, globals and campaigns in one request, with the schema, examples, errors and limits.

Source: https://docs.bifrostorbit.com/cms/developers/graphql/

GraphQL reads the same content as the [Delivery API](https://docs.bifrostorbit.com/cms/developers/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.

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

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

```bash
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:

```bash
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:

```json
{ "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](https://docs.bifrostorbit.com/cms/developers/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.

| 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](https://docs.bifrostorbit.com/cms/developers/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](https://docs.bifrostorbit.com/cms/developers/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

| 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

| 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

| Field | Type |
| --- | --- |
| `key`, `name`, `contentType` | `String!` |
| `locale` | `String!`, the language that answered |
| `fields`, `components` | `JSON!` |

### Navigation and 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

| Type | Field | Type |
| --- | --- | --- |
| `Region` | `key`, `name` | `String!` |
| | `locales` | `[Locale!]!` |
| `Locale` | `code`, `name` | `String!` |
| | `isDefault` | `Boolean!` |

### Resolution and Redirect

| 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.

```graphql
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)
}
```

```json
{ "url": "/products/hiking-boots", "region": "de" }
```

### A paged list

```graphql
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`:

```graphql
{
  pages(type: "faqEntry", region: "de", perPage: 50) {
    items {
      id
      name
      fields
    }
  }
}
```

### A page with the entries it references

```graphql
{
  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

```graphql
{
  global(key: "footer", region: "de") {
    name
    locale
    fields
    components
  }
}
```

### Before rendering an address

```graphql
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`.

```graphql
query Preview($url: String!) {
  page(url: $url, region: "de", draft: true) {
    name
    version
    components
  }
}
```

See [Set up the live preview](https://docs.bifrostorbit.com/cms/developers/preview/).

## From the SDK

```ts
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: true` sends the client's preview key instead of its public key. It does not add `draft: 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: `CmsAuthError` for `401`, `CmsRateLimitError` for `429`, `CmsUnavailableError` for `500` or when the API cannot be reached, and `CmsError` with the status and message for anything else, such as a missing page.

See the [SDK](https://docs.bifrostorbit.com/cms/developers/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:

```json
{
  "data": { "page": null },
  "errors": [
    {
      "message": "Content not found",
      "locations": [{ "line": 1, "column": 3 }],
      "path": ["page"],
      "extensions": { "code": "not_found" }
    }
  ]
}
```

| 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](https://docs.bifrostorbit.com/cms/developers/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`, `global` or `navigation` fails, that field is `null` and the other fields still answer, so a missing menu does not lose the page.
- `pages`, `regions`, `resolve`, `sitemap`, `banners` and `popups` never answer `null`, so when one of them fails the whole `data` is `null`.
- When several fields fail, the HTTP status comes from the first error.
- `page`, `global` and `navigation` report a missing item as an error rather than a quiet `null`. Check `errors` before using `data`.

## 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](https://docs.bifrostorbit.com/cms/developers/delivery-api/).
- `perPage` in `pages` is 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](https://docs.bifrostorbit.com/cms/developers/webhooks/). For pages a CDN should cache, the REST endpoints carry cache headers.
