Reference
Delivery API
Every endpoint websites read content from, with parameters, example answers, keys, fallback, caching, rate limits, limits, timeouts and errors.
Reference26 min read
The delivery API is how a website reads published content. It is read only. Every endpoint answers GET requests with JSON.
https://api.bifrostorbit.com/cms/v1https://api-stage.bifrostorbit.com/cms/v1https://api.bifrostorbit.com/cms/graphql, see GraphQL| Address | |
|---|---|
| Production | https://api.bifrostorbit.com/cms/v1 |
| Staging | https://api-stage.bifrostorbit.com/cms/v1 |
| GraphQL | https://api.bifrostorbit.com/cms/graphql, see GraphQL |
Try any endpoint in Configuration › API playground. Paste a key, pick an endpoint and run it.
Endpoints
Each endpoint takes the common parameters listed in the last column, and its own parameters in its section. Errors are the same everywhere: see Errors.
region, locale, strict, draft, include=referencesregion, locale, strict, draft, include=referencesregion, locale, draft, include=fields,referencesregion, locale, strict, draft, include=references| Endpoint | Answers with | Common parameters |
|---|---|---|
/resolve | What an address is: a page, a redirect, gone or not found | region, locale, strict, draft, include=references |
/content | One page by its address | region, locale, strict, draft, include=references |
/list | Entries of one content type | region, locale, draft, include=fields,references |
/global | A global entry, such as the footer | region, locale, strict, draft, include=references |
/navigation | A menu | region, locale, draft |
/regions | The regions and languages the key may read | None |
/taxonomies/{key} | A taxonomy and its terms | locale |
/schema | Content types and components with their fields | None |
/banners | Banners live now | region, locale, draft |
/popups | Popups live now | region, locale, draft |
/products | Shop products with prices and stock | region, locale, draft |
/sitemap | The sitemap index, or one region's sitemap | region, locale |
/robots | The robots.txt | None |
Leave the trailing slash off endpoint addresses. /cms/v1/content/ answers 308 with a redirect to /cms/api/cdn/v1/content, the same endpoint under its internal address, which costs a second request.
Authentication
Send a delivery key with every request, in either header:
Authorization: Bearer cms_pk_…x-api-key: cms_pk_…When both are sent, Authorization wins. A request with no key, an unknown key, a deleted key or an expired key gets 401.
curl 'https://api.bifrostorbit.com/cms/v1/content?url=/products/hiking-boots®ion=de' \ -H 'Authorization: Bearer cms_pk_…'From a browser
Any website may call the API from browser code with a public key: every answer allows all origins, and preflight requests are answered. Use it for a static site, a widget or anything that reads published content in the browser.
A secret key only works from a server. Sent from another website's browser, it is refused with 401, because a key in browser code can be read by anyone. Keep drafts and preview on your server.
Keys and environments
Create keys in Configuration › API keys.
cms_pk_cms_sk_draft=true. Keep it on the server.| Key | Starts with | Reads |
|---|---|---|
| Public | cms_pk_ | Published content only |
| Secret | cms_sk_ | Published content, and drafts when you add draft=true. Keep it on the server. |
- Each key belongs to one environment and only ever sees that environment's content. Use a staging key against a staging environment and a production key against production.
- A secret key cannot be created for a production environment. Drafts are read from staging environments only.
- A key can be limited to some regions and languages. Asking for any other region or language gets
401, and/regionslists only the ones the key may read. - A key can have an expiry date. After it, the key gets
401.
See Security and access.
Common parameters
Most endpoints take these query parameters. The endpoints table says which apply to each. This is the only table of them; the sections below list only each endpoint's own parameters.
regiongermany or de. Not case sensitive.localede-DE. Not case sensitive.stricttruedrafttrueincludefields and references. See Lists and References.| Parameter | Type | Default | Means |
|---|---|---|---|
region | string | The space's first region | Region key or its short code, such as germany or de. Not case sensitive. |
locale | string | The region's default language | Language code, such as de-DE. Not case sensitive. |
strict | true | off | Refuse every fallback: the page must exist in the language and region asked for. |
draft | true | off | Read drafts instead of published content. Needs a secret key. |
include | list | nothing | Extra content to add to the answer, separated by commas: fields and references. See Lists and References. |
Any value other than true counts as off for strict and draft. A region or language that does not exist, or anything other than fields or references in include, is an error.
Pages
GET/resolve
Says what an address is: a page, a redirect, gone or not found. Call it before rendering a page, so redirects and removed pages behave correctly.
url//products/hiking-boots. A missing leading slash is added and a trailing slash is removed.| Parameter | Type | Default | Means |
|---|---|---|---|
url | string | / | The page address, such as /products/hiking-boots. A missing leading slash is added and a trailing slash is removed. |
Optional header: x-cms-referrer with the address of the page that linked here. It is recorded with addresses that are not found, so editors can see where broken links come from.
curl 'https://api.bifrostorbit.com/cms/v1/resolve?url=/products/hiking-boots®ion=de' \ -H 'Authorization: Bearer cms_pk_…'The HTTP status follows the answer's kind:
page200page holds the same page as /contentredirect200destination with statusCodegone410notFound404kind | Status | Means |
|---|---|---|
page | 200 | page holds the same page as /content |
redirect | 200 | Send the visitor to destination with statusCode |
gone | 410 | The page was published once and has been removed, or a campaign page has ended |
notFound | 404 | Nothing was ever published here |
A page. page is the same object /content answers with:
{ "data": { "kind": "page", "page": { "url": "/products/hiking-boots", "name": "Hiking boots", "…": "…" } } }A redirect. permanent is true for 301 and 308, and hops counts the redirects that were followed to reach the final address:
{ "data": { "kind": "redirect", "destination": "/products/hiking-boots", "statusCode": 301, "permanent": true, "hops": 1 }}Gone and not found carry only the kind:
{ "data": { "kind": "notFound" } }GET/content
One page by its address. Use it when you only need the page and handle redirects yourself.
url//resolve| Parameter | Type | Default | Means |
|---|---|---|---|
url | string | / | The page address, read as in /resolve |
curl 'https://api.bifrostorbit.com/cms/v1/content?url=/products/hiking-boots®ion=de&locale=de-DE' \ -H 'Authorization: Bearer cms_pk_…'{ "data": { "id": "6650c0f2a1b2c3d4e5f60718", "url": "/products/hiking-boots", "name": "Hiking boots", "contentType": "page", "locale": "de-DE", "region": "germany", "requestedLocale": "de-DE", "requestedRegion": "germany", "fallbackApplied": false, "regionFallbackApplied": false, "draft": false, "publishedAt": "2026-10-01T09:30:00.000Z", "version": 7, "fields": { "title": "Find a shop", "heroImage": { "id": "6650c1…", "url": "https://…/hero.jpg", "mimeType": "image/jpeg", "width": 1600, "height": 900, "alt": "A pair of hiking boots", "title": null, "subtitles": null } }, "components": [ { "id": "c1", "type": "hero", "schemaVersion": 1, "fields": { "heading": "Free boot fitting" }, "children": {}, "variant": null } ], "seo": { "title": "Hiking boots | Acme Outdoors", "description": "Find a shop near you.", "canonical": "https://www.example.com/de/products/hiking-boots", "robots": { "index": true, "follow": true }, "openGraph": { "type": "website", "title": "Hiking boots", "description": "Find a shop near you.", "url": "https://www.example.com/de/products/hiking-boots", "siteName": "Acme Outdoors", "locale": "de_DE", "image": null }, "twitter": { "card": "summary_large_image", "site": null, "title": "Hiking boots", "description": "Find a shop near you.", "image": null }, "verification": { "google": null, "bing": null }, "alternates": [{ "hreflang": "de-DE", "href": "https://www.example.com/de/products/hiking-boots" }], "breadcrumbs": [{ "name": "Products", "url": "https://www.example.com/de/products" }], "jsonLd": [] }, "insights": null }}idurl, name, contentTypelocale, regionrequestedLocale, requestedRegionfallbackAppliedtrue when another language answered. See Fallback.regionFallbackAppliedtrue when another region answereddrafttrue for a draftpublishedAtnull if neverversionfieldsurl, size and alt; links carry the target page's current address. See Field types.componentsid, type, schemaVersion, fields, children (lists by slot) and variant. Blocks in a slot arrive both in children and on the slot's field. See Components and slots in Field types. Shared sections arrive already in place: their blocks appear where the section was placed, and the sharedSection block itself is never delivered.seoinsights{ "site": "…" } when the space is connected to Insights Orbit, with the site key the script counts visits with; otherwise null. The key is public, like the script tag it goes in. See Insights Orbit.expired{ "message": "This offer has ended." }| Field | Means |
|---|---|
id | The entry's id in this region and language, the same as in the management API |
url, name, contentType | The page's address, its name in the CMS and its content type key |
locale, region | The language and region that answered |
requestedLocale, requestedRegion | What was asked for, or the defaults when nothing was |
fallbackApplied | true when another language answered. See Fallback. |
regionFallbackApplied | true when another region answered |
draft | true for a draft |
publishedAt | When it was last published, or null if never |
version | The published version number, or the draft version for a draft |
fields | The content type's fields. Images become objects with url, size and alt; links carry the target page's current address. See Field types. |
components | The page body: a tree of components with id, type, schemaVersion, fields, children (lists by slot) and variant. Blocks in a slot arrive both in children and on the slot's field. See Components and slots in Field types. Shared sections arrive already in place: their blocks appear where the section was placed, and the sharedSection block itself is never delivered. |
seo | Ready-made metadata: title, description, canonical, robots, Open Graph, X card, hreflang alternates, breadcrumbs and JSON-LD |
insights | { "site": "…" } when the space is connected to Insights Orbit, with the site key the script counts visits with; otherwise null. The key is public, like the script tag it goes in. See Insights Orbit. |
expired | Only on a campaign page past its end date that is set to stay visible: { "message": "This offer has ended." } |
Nothing at the address is a 404 error.
Lists
GET/list
Entries of one content type, such as products, articles or FAQ entries. Items are summaries without a body; ask /content for a full page. Add include=fields to get each entry's fields too, which is how you read entries that have no address of their own, such as FAQ entries.
typeproduct. Required in practice: without it the list is empty.page1perPage20filter[<field>]tagtags field contains this valuecategorycategory field equals this valueqincludefields adds each entry's fields. references also replaces the references in them with the entries they point at, and implies fields.| Parameter | Type | Default | Limits and notes |
|---|---|---|---|
type | string | Content type key, such as product. Required in practice: without it the list is empty. | |
page | integer | 1 | 1 or more |
perPage | integer | 20 | 1 to 100. Larger values are capped at 100. |
sort | string | sortOrder | See Sorting |
filter[<field>] | string | Exact match on a field. Repeat for several fields; all must match. | |
tag | string | Entries whose tags field contains this value | |
category | string | Entries whose category field equals this value | |
q | string | Entries whose name contains this text, ignoring case | |
include | list | fields adds each entry's fields. references also replaces the references in them with the entries they point at, and implies fields. |
Lists do not fall back to another language.
curl 'https://api.bifrostorbit.com/cms/v1/list?type=product®ion=de&sort=-publishedAt&perPage=10&filter[activity]=hiking' \ -H 'Authorization: Bearer cms_pk_…'{ "data": { "items": [ { "id": "66f1c0ffee0000000000a001", "url": "/products/hiking-boots", "name": "Hiking boots", "contentType": "product", "publishedAt": "2026-09-14T11:02:00.000Z", "seo": { "title": "Hiking boots", "description": "…" } } ], "total": 42, "page": 1, "hasMore": true }}An entry without its own address has url and seo set to null. Global entries never appear in lists. id is the entry's id, the same value a Reference field holds.
Entries with their fields
curl 'https://api.bifrostorbit.com/cms/v1/list?type=faqEntry®ion=de&category=pricing&include=fields' \ -H 'Authorization: Bearer cms_pk_…'{ "data": { "items": [ { "id": "66f1c0ffee0000000000b007", "url": null, "name": "Can I cancel at any time?", "contentType": "faqEntry", "publishedAt": "2026-09-30T08:00:00.000Z", "seo": null, "fields": { "question": "Can I cancel at any time?", "answer": "<p>Yes. Your plan ends at the close of the month.</p>", "category": "pricing" } } ], "total": 1, "page": 1, "hasMore": false }}fields are delivered as on a page: images become objects and links carry the target page's address. Without include=fields the items look exactly as before, so existing websites are not affected.
Pagination
Ask for page=1, then the next page while hasMore is true. total is the number of matching entries across all pages.
Sorting
sortOrdername, -namepublishedAt, -publishedAtupdatedAt, -updatedAtsort | Order |
|---|---|
sortOrder | The order editors set by hand, then by name. The default. |
name, -name | By name, A to Z or Z to A |
publishedAt, -publishedAt | Oldest or newest published first, then by name |
updatedAt, -updatedAt | Least or most recently changed first, then by name |
Any other value is a 400 error.
Filtering
filter[<field>]=<value>matches the field's key as set on the content type, such asfilter[activity]=hiking. A key must start with a letter and contain only letters, digits and underscores, otherwise the request is a400error.- Values are compared as text and must match exactly. A list field matches when it contains the value.
filter,tag,categoryandqcombine: an entry must match all of them.
References
Reference and References fields hold entry ids. Add include=references to /resolve, /content, /global or /list and each id is replaced by the entry it points at, wherever the field is: on the entry itself or in any component of its body.
curl 'https://api.bifrostorbit.com/cms/v1/resolve?url=/pricing®ion=de&include=references' \ -H 'Authorization: Bearer cms_pk_…'A component with a References field called questions then arrives like this:
{ "id": "c4", "type": "faqList", "schemaVersion": 1, "fields": { "heading": "Questions about plans", "questions": [ { "id": "66f1c0ffee0000000000b007", "url": null, "name": "Can I cancel at any time?", "contentType": "faqEntry", "publishedAt": "2026-09-30T08:00:00.000Z", "fields": { "question": "Can I cancel at any time?", "answer": "<p>Yes.</p>", "category": "pricing" } } ] }, "children": {}, "variant": null}- Each entry has its
id,url(ornullwithout an address),name,contentType,publishedAtand itsfields, delivered as on a page. Its body is not included; ask for its page when it has one. - One level only: references inside the referenced entry stay ids.
- The entry is read in the same region, and in the language of the request, following the same language fallback as the page. An entry that is translated answers in the visitor's language even though editors picked it in another one, and a page promoted from staging points at production's copy of each entry.
- Published content shows published entries only. An entry that is unpublished, deleted, missing in the region or in a language the key may not read is left out of a References list, and a Reference becomes
null. - With
draft=true, the referenced entries' drafts are shown too. - Without
include=referencesreferences stay ids, as before.
To refresh pages when an entry they show is published, see Refresh the website when content changes. The SDK does this for you.
Site-wide content
GET/global
A global entry, such as the footer or the site header, by its key.
key| Parameter | Type | Default | Means |
|---|---|---|---|
key | string | The global entry's key. Required. |
It follows the language fallback, but not the region fallback.
curl 'https://api.bifrostorbit.com/cms/v1/global?key=footer®ion=de' \ -H 'Authorization: Bearer cms_pk_…'{ "data": { "key": "footer", "name": "Footer", "contentType": "footer", "locale": "de-DE", "fields": { "copyright": "© Acme Outdoors" }, "components": [] }}locale is the language that answered, which may be a fallback.
GET/navigation
A menu, by its key. A menu can be shared by all regions or belong to one.
keymain. Required.| Parameter | Type | Default | Means |
|---|---|---|---|
key | string | The menu key, such as main. Required. |
curl 'https://api.bifrostorbit.com/cms/v1/navigation?key=main®ion=de' \ -H 'Authorization: Bearer cms_pk_…'{ "data": { "key": "main", "name": "Main menu", "items": [ { "label": "Services", "href": "/services", "target": null, "icon": null, "badge": null, "description": null, "children": [ { "label": "Hiking boots", "href": "/products/hiking-boots", "target": null, "icon": null, "badge": "New", "description": "Fitted by people who hike", "children": [] } ] } ] }}Items that link to a page carry its current address, so renaming a page never breaks the menu. Hidden items, and items linking to a page that is not published in this region and language, are left out.
GET/regions
The regions the key may read, with their languages. No parameters.
{ "data": [ { "key": "germany", "name": "Germany", "locales": [ { "code": "de-DE", "name": "German (Germany)", "isDefault": true }, { "code": "en-DE", "name": "English", "isDefault": false } ] } ]}GET/taxonomies/{key}
A taxonomy and its terms, in their set order.
locale| Parameter | Type | Default | Means |
|---|---|---|---|
locale | string | Language for the term labels. Terms without a label in that language use their default label. |
{ "data": { "key": "activity", "name": "Activity", "terms": [{ "value": "hiking", "label": "Hiking", "sortOrder": 0 }] }}GET/schema
The space's content types and components, with their fields. The SDK's type generator uses it. No parameters. Deprecated components and hidden fields are left out.
{ "data": { "contentTypes": [ { "key": "page", "name": "Page", "description": null, "isGlobal": false, "fields": [] } ], "components": [ { "key": "hero", "name": "Hero", "description": null, "group": "Layout", "allowedChildren": "*", "fields": [] } ] }}Each field has key, name, type, required, description, options, allowedComponents, allowedContentTypes, taxonomy and validation.
Campaigns
Banners and popups are worked out for the moment of the request. Both answers include serverTime, so countdowns can tick from the CMS clock rather than the visitor's.
GET/banners
Banners live now for a page, pinned first and then by priority.
path/everyPagetruepaths it targets, so a website can read them once for all its pages and pick per page itself. path is then ignored.loggedIntrue| Parameter | Type | Default | Means |
|---|---|---|---|
path | string | / | The page being shown. Only banners targeting it are returned. |
everyPage | true | off | Return the banners live on any page instead, each with the paths it targets, so a website can read them once for all its pages and pick per page itself. path is then ignored. |
loggedIn | true | off | Whether the visitor is signed in. Blocks meant only for guests or only for signed-in visitors are left out accordingly. |
curl 'https://api.bifrostorbit.com/cms/v1/banners?path=/products/hiking-boots®ion=de' \ -H 'Authorization: Bearer cms_pk_…'{ "data": { "serverTime": "2026-10-17T10:00:00.000Z", "items": [ { "id": "66f1a2…", "blocks": [ { "id": "b1", "type": "message", "text": "Free boot fitting this week", "segments": [{ "text": "Free boot fitting", "highlight": true }, { "text": " this week", "highlight": false }] } ], "text": "Free boot fitting this week", "segments": [{ "text": "Free boot fitting", "highlight": true }, { "text": " this week", "highlight": false }], "link": null, "background": "#1f2937", "foreground": "#ffffff", "placement": "aboveHeader", "paths": ["/services/*"], "pinned": false, "priority": 0, "trackingLabel": "consult-week", "countdown": null, "endsAt": "2026-10-20T22:59:00.000Z", "next": null } ] }}foreground is the text colour the editor chose, or white or dark, whichever reads best on background. placement is where the editor wants it: aboveHeader or belowHeader. paths are the pages it targets, with /* at the end for a whole section, and empty for every page.
next is the banner that takes over when this one ends, with its startsAt, so the website can switch without asking again. See Render banners, popups and countdowns.
GET/popups
Popups live now for a page and the visitor's sign-in state, highest priority first.
path/loggedIntrue| Parameter | Type | Default | Means |
|---|---|---|---|
path | string | / | The page being shown |
loggedIn | true | off | Whether the visitor is signed in. Popups for guests only or signed-in visitors only are filtered by it. |
Each item has id, name, layout, style, minimisedText, blocks, priority, hideForDays (default 30), maxViews, trackingLabel, startsAt and endsAt.
Shop
GET/products
Shop products with live prices and stock, in their set order.
page1perPage24filter| Parameter | Type | Default | Limits and notes |
|---|---|---|---|
page | integer | 1 | 1 or more |
perPage | integer | 24 | 1 to 100. Larger values are capped at 100. |
filter | string | A taxonomy term value. Products tagged with it in any taxonomy field match. |
The answer has items, total, page and hasMore, like /list. Each product has slug, url, name, catalogId, size, price, currency, wasPrice, inStock, onSale, badges, benefits, directions, ingredients, terms, images, filters (with labels), relatedSlugs, seo and publishedAt. catalogId links the product to your shop catalogue, which supplies price and inStock. Both are null when the product is not linked or the catalogue cannot be reached, and currency is then the region's currency.
Search engines
GET/sitemap
Without region, the index of every region and language. With region, that region's entries.
regionlocalepage1| Parameter | Type | Default | Means |
|---|---|---|---|
region | string | Region key or code. Leave out for the index. | |
locale | string | The region's default language | Language of the sitemap |
page | integer | 1 | Sitemap file number. Each holds up to 50,000 addresses. |
The index:
{ "data": { "indexable": true, "sitemaps": [{ "region": "germany", "locale": "de-DE", "base": "https://www.example.com/de", "pages": 312, "files": 1 }] }}A region's sitemap:
{ "data": { "region": "germany", "locale": "de-DE", "page": 1, "totalPages": 1, "entries": [ { "loc": "https://www.example.com/de/products/hiking-boots", "lastmod": "2026-10-01T09:30:00.000Z", "changefreq": "monthly", "priority": null, "images": [], "alternates": [{ "hreflang": "de-DE", "href": "https://www.example.com/de/products/hiking-boots" }] } ] }}Only published pages with an address are listed. Pages set to no-index or left out of the sitemap are skipped. Sitemaps always show published content.
GET/robots
The robots.txt for the environment. No parameters.
{ "data": { "indexable": true, "content": "User-agent: *\nAllow: /\nSitemap: https://www.example.com/de/sitemap.xml\n" }}An environment that is not indexable, such as staging, always gets User-agent: * and Disallow: /.
Fallback
When a page is missing in the language or region asked for, the API can answer with another one rather than 404. Every page answer says what happened.
- Language fallback. Each language can name a fallback language in the same region. The API tries the language asked for, then its fallback, then that language's fallback, and so on. The first one with published content answers. A space can turn this off with Allow locale fallback in its settings, and then every request behaves as if it sent
strict=true. - Region fallback. If no language in the region has the page, and the space is set to show another region's page when one is missing, the API tries that region in its default language. See Space settings.
strict=trueturns both off.
/content, /resolve)/global| Endpoint | Language fallback | Region fallback |
|---|---|---|
Pages (/content, /resolve) | Yes | Yes |
/global | Yes | No |
| Everything else | No | No |
On a page, compare the fields to see what answered:
requestedRegion, requestedLocalegermany, en-DEregion, localegermany, de-DEfallbackAppliedtrueregionFallbackAppliedtrue| Field | Example | Means |
|---|---|---|
requestedRegion, requestedLocale | germany, en-DE | What was asked for |
region, locale | germany, de-DE | What answered |
fallbackApplied | true | Another language answered |
regionFallbackApplied | true | Another region answered |
The SDK copies these into page.meta, so a website can show a notice such as "This page is not available in English yet." A key limited to some languages never falls back to a language it may not read.
Drafts
Add draft=true with a secret key to read the latest saved draft instead of the published version. Use it for previews only.
- A public key asking for drafts gets
401. See Errors. - Drafts are never cached.
- Pages that were never published are found, and campaign pages show outside their start and end dates, so editors can preview ahead of time.
/regions,/schema,/sitemap,/robotsand/taxonomies/{key}always answer with published content.
Caching
Successful answers tell caches how long they may keep them. Every answer also has Vary: Authorization.
/content, /resolve, /list, /banners, /popups, /products, /taxonomies/{key}Cache-Controlpublic, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400/global, /navigation, /schema, /sitemap, /robotsCache-Controlpublic, s-maxage=300, stale-while-revalidate=1500, stale-if-error=86400/regionsCache-Controlpublic, s-maxage=3600, stale-while-revalidate=18000, stale-if-error=86400draft=trueCache-Controlprivate, no-store/resolve answering notFoundCache-Controlprivate, no-store429Cache-Controlno-store| Endpoints | Cache-Control |
|---|---|
/content, /resolve, /list, /banners, /popups, /products, /taxonomies/{key} | public, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400 |
/global, /navigation, /schema, /sitemap, /robots | public, s-maxage=300, stale-while-revalidate=1500, stale-if-error=86400 |
/regions | public, s-maxage=3600, stale-while-revalidate=18000, stale-if-error=86400 |
Any request with draft=true | private, no-store |
/resolve answering notFound | private, no-store |
Errors, including 429 | no-store |
In words: a shared cache, such as a CDN in front of your website, may keep a published answer for 60 seconds (5 minutes for menus, globals and sitemaps, an hour for regions). After that it may serve the old answer for five times as long while it fetches a new one, and for a day if the API is failing. A newly created page is found as soon as it is published, because not found is never cached.
The API does not send ETag or Last-Modified, so conditional requests are not supported.
To show a change at once rather than within these times, listen for publish webhooks and refresh only what changed. See Refresh the website when content changes.
Rate limits
Each key and each client IP address may make 15,000 requests a minute, counted separately. A request counts against both, and the stricter one applies. GraphQL requests count too, against the same limit. Windows are fixed calendar minutes, so the count resets at the start of each minute.
Going over gets 429:
HTTP/1.1 429 Too Many RequestsCache-Control: no-storeRetry-After: 23X-RateLimit-Limit: 15000X-RateLimit-Remaining: 0X-RateLimit-Reset: 23{ "error": { "code": "rate_limited", "message": "Too many requests; try again shortly" } }Every answer from an endpoint, not only a 429, says where you stand:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After429 only: the seconds to wait, the same as X-RateLimit-Reset| Header | Meaning |
|---|---|
X-RateLimit-Limit | The requests allowed this minute, from the stricter of the key and the IP address |
X-RateLimit-Remaining | The requests left this minute, counting this one |
X-RateLimit-Reset | Whole seconds until the minute ends and the count starts again, from 1 to 60 |
Retry-After | On a 429 only: the seconds to wait, the same as X-RateLimit-Reset |
Browser code can read all four. An answer served from a cache in front of the API, such as your own CDN, keeps the values it had when it was cached.
Cache answers in your website and you will stay well below the limit.
Limits and timeouts
perPage in /list and /products| Limit | Value |
|---|---|
| Requests | 15,000 a minute per key and per IP address. See Rate limits. |
perPage in /list and /products | At most 100. Larger values are capped at 100. |
| Addresses in one sitemap file | 50,000 |
| References | One level. See References. |
| GraphQL query | At most 20,000 characters, 10 levels deep and 300 fields. See GraphQL. |
The API sets no limit of its own on the length of the address or query string, the length of q, or the number of filter[…] parameters. Keep addresses short all the same: very long ones can be refused by the network in front of the API before they reach it.
Timeouts:
- The SDKs: In the JavaScript, PHP, Swift and Kotlin SDKs each attempt times out after 5 seconds. Network errors, timeouts and server errors are retried twice, after 250 milliseconds and then 750 milliseconds. A
429is not retried, and GraphQL requests from the JavaScript SDK are tried once. Each SDK lets you change the timeout and the number of retries; see the SDK. - The API: It sets no time limit of its own on a request.
- While the API restarts: During a release the gateway holds a request for up to 30 seconds while it waits for the API to come back, so a request can take that long without failing.
Errors
Errors from the API come as JSON with the HTTP status, Cache-Control: no-store and the same browser headers as every other answer:
{ "error": { "code": "not_found", "message": "Content not found" } }400codebad_requestsort, a filter[…] name that is not a valid field key, or anything other than fields or references in include401codeunauthorized402codeplan_ended404codenot_foundRegion not found.500codeinternal_errorThe request failed.| Status | code | When |
|---|---|---|
400 | bad_request | An unknown sort, a filter[…] name that is not a valid field key, or anything other than fields or references in include |
401 | unauthorized | No key, an 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 |
402 | plan_ended | The organisation's CMS Orbit plan has ended. An owner can choose a plan under Billing in Bifröst Orbit. |
404 | not_found | Nothing at the address, or no menu, global entry, taxonomy, region or language with that key. The message names which, such as Region not found. |
429 | rate_limited | Too many requests. See Rate limits. |
500 | internal_error | Something failed on our side. Try again; the message is always The request failed. |
/resolve is different: not found and gone are normal answers, { "data": { "kind": "notFound" } } with 404 and { "data": { "kind": "gone" } } with 410, not errors.
GraphQL errors have their own shape. See GraphQL.
Answers that do not come from an endpoint
A few answers come from before a request reaches an endpoint. Check the status before reading the body.
https://api.bifrostorbit.com/cms/v2/content, or an endpoint that does not exist under /cms/v1/, such as /cms/v1/pages404 in the usual shape, with a link to these docs: { "error": { "code": "not_found", "message": "…" }, "docs": "…" }/cms/v1/content/308 redirect. See Endpoints.5xx status, such as 502, without a JSON body| Request | Answer |
|---|---|
An address outside the API, such as https://api.bifrostorbit.com/cms/v2/content, or an endpoint that does not exist under /cms/v1/, such as /cms/v1/pages | 404 in the usual shape, with a link to these docs: { "error": { "code": "not_found", "message": "…" }, "docs": "…" } |
An endpoint with a trailing slash, such as /cms/v1/content/ | 308 redirect. See Endpoints. |
| Any request while the API cannot be reached for more than 30 seconds | A 5xx status, such as 502, without a JSON body |
The JavaScript SDK handles all of these: it follows the redirect, a 404 throws CmsNotFoundError, and a 5xx is retried and then throws CmsUnavailableError.
Was this page helpful?