Skip to main content

CMS

The Ovok CMS holds the content your app needs that FHIR does not model: legal pages, UI translations, release notes and content types you define yourself. You write it once in the Ovok Console, and your apps read the published version over a public, cacheable API.

What you can read​

CollectionWhat it holdsWho may read it
legal-pagesTerms, privacy and similar pages, with a localised title and body.Anyone who names your tenant code.
translationsUI text, one document per group, with a localised value for each key. See Use Ovok CMS with i18n.Anyone who names your tenant code.
release-notesRelease notes, with tags and a publish date.A CMS API key, or a signed-in user of your project.
Your own content typesItems you define in the Console.A CMS API key, or a signed-in user of your project.

Ovok decides which collections are open to anyone; an operator can change that list for a deployment.

The routes​

RoutePage
GET /v1/public/cms/collections/:collection/itemsList the items of a collection
GET /v1/public/cms/collections/:collection/items/:slugGet an item by slug
GET /v1/public/cms/:contentTypeSlug/itemsList the items of a content type
Use Ovok CMS with i18n

Enable the CMS​

Enable the CMS for your project in the Ovok Console. The Console sets the CONTENT_ENABLED marker, provisions the CMS environment, and manages authoring access and billing. Ovok's public-delivery routes do not read the setting itself. Changing the setting directly through the project settings API is not a substitute for the Console workflow; public delivery also depends on the project's CMS environment.

Your project's content space is created the first time the CMS is used, by the Console or by an app reading content. It needs your project's tenant code to exist, and a brand-new code can be refused as unknown for up to a minute.

Tenants and environments​

Content belongs to a tenant, which is your project, named by its tenant code, and to an environment: dev, staging or prod. Each environment has its own content, so write the content in the same environment the app reads.

You sendWhereMeaning
tenantQuery, or the x-ovok-tenant-code headerYour tenant code.
projectIdQueryYour project's id, a UUID. Use it when you do not have the tenant code. A tenant code takes precedence if both are sent, but the project ID must still be a valid UUID.
environmentQuery, or the x-ovok-environment headerdev, staging or prod.
  • Name the tenant on every read. A signed-in user may leave it out and reads their own project. Everyone else who names nothing gets 400.
  • The environment is optional, and a wrong value is not an error. Anything other than dev, staging or prod is ignored, and the request uses the host's default: prod on the production API, staging on the sandbox. Check the spelling: ?environment=production silently reads the default.
  • Headers beat query values, even empty ones. An empty x-ovok-tenant-code header hides ?tenant=.

Who can read content​

Three ways in, tried in this order:

WayHowReads
A CMS API keyx-api-key: <key>, Authorization: Bearer <key> or ?apiKey=<key>Published content of the tenant you name.
An open collectionNothing. Only the tenant code. Applies to legal-pages and translations by default, and only on the collection routes.Published content of the tenant you name.
A signed-in sessionAn Ovok access token as Authorization: Bearer <token>Published content of the user's own tenant only.

Anything else answers 401.

  • There is one CMS key per Ovok deployment, not one per project. Ask Ovok for it. It reads the published content of any tenant, so keep it on your server and do not ship it in a public client; use an open collection or a signed-in session there.
  • A session that names another tenant gets 403, not 401, so the SDKs do not sign the user out. The exception is an open collection: a signed-in user can read another tenant's legal-pages or translations by naming it.
  • Authentication comes before everything else. GET .../collections/users/items without a key answers 401, even though no such collection is served.

Languages​

The CMS has four languages: de, en, fr and es. en is the default.

  • Pass locale=de. A tag that is not a valid language tag answers 400: en_US, DE and all are refused.
  • Use the bare codes. A tag such as de-DE is accepted, but the CMS does not have it and answers in the default language, English. The Ovok SDKs map de-DE to de for you.
  • A text or textarea field with no value in the language falls back to English field by field.

Published content​

Only documents with status published are returned. A draft, and a document that does not exist, look the same: not found. There are no versions or scheduled publishing: publishedAt is an ordinary date and hides nothing.

Rich text is returned as the editor's JSON structure, not as HTML.

Shared content​

Ovok keeps a shared tenant of its own, which contributes documents to every tenant:

  • On the list routes, its published documents are merged with yours.
  • On the get-by-slug route, it is the fallback when your tenant has no document with that slug.
  • Your document wins when the slug is the same. A translations group of yours replaces the shared group of the same slug whole, so a group with one key hides the shared group's other keys.
  • The merge is skipped when either side has more than 200 published documents, and your own list is returned unmerged.
  • A failing shared read never breaks your read: you get your own documents.

Caching​

ResponseCache-Control
A tenant named by code or project idpublic, s-maxage=60, stale-while-revalidate=300
A signed-in user who names no tenantno-store
An errorNone

A tenant-named response also carries a Vary header on the tenant, project, environment and API key headers. Ovok caches each read for about 60 seconds, so an edit shows up within a minute, plus up to about 5 minutes more behind a CDN. Nothing purges the cache when you publish.

Browsers and CORS​

For an allowed origin, Ovok's CORS policy accepts Content-Type, Authorization, Accept-Language, If-Match, X-Medplum, X-Tenant-Code and the tracing headers. x-api-key, x-ovok-tenant-code and x-ovok-environment are not allowed, so a browser must send:

  • the key as Authorization: Bearer <key> or ?apiKey=;
  • the tenant and environment as ?tenant= and ?environment=.

A key in the URL can end up in logs. Prefer the header from a server.

Limits​

WhatValue
page1 to 100. A non-number falls back to 1.
limit1 to 50. Default 20.
Documents readable per list5,000 (50 per page, 100 pages)
Content type routeAt most 100 items, no paging, no sort, no locale
SlugLower-case letters, digits, -, _ and .; starts with a letter or digit; up to 128 characters
Rate limitKey-only and unauthenticated calls use the unknown-caller limit: 1,000 per minute per route and IP address. Signed-in calls use the caller's role limit per route: patients and related people 100 per 30 seconds; practitioners 200 per 30 seconds; admins and client applications 1,000 per 30 seconds. Over a limit returns 429.

Errors​

StatusmessageCause
400Public CMS delivery requires a tenant code, a project id, or a signed-in sessionNo tenant is named and the caller is not signed in.
400Unknown tenant code: <code>The tenant code does not exist.
400Invalid project id. followed by what was expectedprojectId is not a UUID.
400Invalid locale "<value>". Expected a language tag like "de" or "en-US".The language tag is malformed, or is all.
400Invalid sort "<value>". Expected a field name, optionally prefixed with "-".sort is not a top-level field name.
400Environment "<env>" is suspended for this projectThe environment has been suspended. Content is not deleted.
401Invalid or missing CMS public API keyThe collection is not open and there is no valid key or session.
401CMS public API key is not configuredThe deployment has no key, and the request needs one.
403A signed-in session reads its own tenant's contentA signed-in user named another tenant.
404Not foundA missing, unpublished or malformed slug, or a collection that is not served.
429Too Many RequestsOver the rate limit.
502Unable to resolve the CMS tenantThe CMS is unreachable. Retry.

Errors share the format described in errors and troubleshooting: statusCode, error, timestamp, path, message and requestId. A CMS failure is never answered with an empty 200.