Skip to main content

Public delivery

Public delivery is the surface your frontend hits. It serves published content only, authenticates with a project-scoped API key (no JWT, no user session), and is safe to call from browsers and mobile apps.

GET https://api.sandbox.ovok.com/v1/public/cms/{typeSlug}/items?environment=dev&projectId={medplumProjectId}
GET https://api.sandbox.ovok.com/v1/public/cms/{typeSlug}/items/{idOrSlug}?environment=dev&projectId={medplumProjectId}

Required query parameters

ParamRequiredDescription
environmentYesdev, staging, or prod — selects the content partition
projectIdYesMedplum project UUID; resolves the Payload tenant

Use projectId, not projectSlug, on this surface.

Authentication

Send the API key as Authorization: Bearer …, x-api-key: …, or ?apiKey=…. Each key is bound to exactly one project when PAYLOAD_CMS_PUBLIC_API_KEY is configured on ovok-core (sandbox may allow unauthenticated access when that env var is unset).

Mint keys from the Console at Settings → API keys → CMS:

Console Settings API keys tab with CMS API keys card and a New API key button

GET /v1/public/cms/pages/items?environment=dev&projectId=4897826b-4ff5-4ce4-a96a-849a40f51f60 HTTP/1.1
Host: api.sandbox.ovok.com
Authorization: Bearer cmspub_a4f3b21c...
Accept: application/json

How to mint a key →

:::tip Key safety API keys identify your project. They're meant to be shipped in your frontend bundle — they only grant read access to published content. But: rotate them on schedule, scope them per app, and never check them into a public repo. :::

List items in a collection

curl "https://api.sandbox.ovok.com/v1/public/cms/pages/items?environment=dev&projectId=$MEDPLUM_PROJECT_ID" \
-H "Authorization: Bearer $OVOK_CMS_KEY"

Additional query params:

ParamTypeDescription
limitintegerMax items per page (default 10, max 100)
pageinteger1-indexed page number
sortstringSort field, prefix - to reverse (-publishedAt)
where[field][op]JSONPayload's standard filter syntax

Example — recent published blog posts, newest first:

curl "https://api.sandbox.ovok.com/v1/public/cms/posts/items?environment=dev&projectId=$MEDPLUM_PROJECT_ID&sort=-publishedAt&limit=5" \
-H "Authorization: Bearer $OVOK_CMS_KEY"

Get one item

By ID:

curl "https://api.sandbox.ovok.com/v1/public/cms/pages/items/abc123?environment=dev&projectId=$MEDPLUM_PROJECT_ID" \
-H "Authorization: Bearer $OVOK_CMS_KEY"

By slug (Payload's slug field, if your collection defines one):

curl "https://api.sandbox.ovok.com/v1/public/cms/pages/items/pricing?environment=dev&projectId=$MEDPLUM_PROJECT_ID" \
-H "Authorization: Bearer $OVOK_CMS_KEY"

The endpoint resolves slug vs ID automatically. Drafts and unpublished items return 404 here regardless of whether you ask by ID or slug.

What's not returned

Public delivery hides the bits authors don't want public:

  • Items with _status: draft or _status: published but publishedAt in the future
  • Soft-deleted items
  • Internal-only fields (anything marked admin.hidden in the Payload collection config)
  • Items not in the key's project or wrong environment

If you can read an item via /v1/content/api/... but not via /v1/public/cms/.../items/..., check draft status, environment, and projectId first.

CORS

Public delivery is the only CMS surface with browser-friendly CORS. The proxy returns Access-Control-Allow-Origin: * for GET and HEAD. If you need a stricter origin set, terminate at your own edge (Vercel, Cloudflare) and lock CORS there.

Caching

Responses include Cache-Control: public, max-age=60, s-maxage=300 by default — a minute on the client, five minutes on shared caches. Publishing an item invalidates the shared cache for that item's collection via a fan-out hook. Custom cache headers per collection are on the roadmap.

Errors

StatusMeaning
401API key missing or revoked
403API key belongs to a project where CMS is disabled
404Item doesn't exist, isn't published, or belongs to another project
429Rate-limited — back off and retry
502Tenant unreachable — retry

Next