Diil Docs
  1. Docs
  2. Getting started

Authentication with site keys

Updated:

Every request to the Content API carries a site key. It tells us which site you are reading from — and that is pretty much all it does. No OAuth dance, no token refresh, no signing requests at midnight. One string in one header, and you are in.

What a site key is

A site key looks like this:

pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
  • The prefix pk_ followed by exactly 32 lowercase hex characters: ^pk_[a-f0-9]{32}$.
  • It belongs to one site. The key alone decides whose content you get — there is no “site id” parameter anywhere in the API.
  • It is read-only and only sees published content.
  • A site can have several keys at once, which makes painless rotation possible (more on that below).

The pk_ is a nod to “publishable keys” you may know from payment providers: a key that is designed to live in public code. We will get to why that is fine in a minute.

Where to get a key

  1. Open your site in the CRM

    Sign in to Diil and pick the site whose content you want to read.
  2. Go to Settings → Content API keys

    This is where all keys of the site live.
  3. Create a key

    You get a fresh pk_… string. Copy it.
  4. Put it into an environment variable

    Not straight into the code — future you, rotating keys, will be grateful. Patterns for popular frameworks are below.

How to send the key

Put the key into the x-crm-key request header. That is the standard way, and it works from servers and browsers alike — CORS allows the header from any origin.

curl "https://back.sitecog.com/content/v1/pages/home?lang=en" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

The ?key= fallback

If you really can't set a header, pass the key as a query parameter instead:

curl "https://back.sitecog.com/content/v1/pages/home?lang=en&key=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

When is that okay?

  • Quick checks — pasting a URL into the browser address bar to see what an endpoint returns.
  • Tools that only take a URL — a no-code integration, a feed importer, a static site generator plugin with no header option.

Everywhere else, prefer the header. URLs tend to end up in server logs, browser history and analytics, and although the key is not a secret, there is no reason to sprinkle it all over the place. It also keeps your URLs short and your code tidy.

Why the key is safe in the browser

Short answer: because it can't do anything your visitors can't already do by opening your site. A site key is public by design. Here is what it can and cannot do:

A site key…
reads published pages, sections, blocks and blog posts of its siteyes
changes, creates or deletes anythingno — the API is read-only
sees drafts or unpublished blog postsno — published content only
reads content of your other sitesno — one key, one site

It is the same idea as a payment provider's publishable key: it identifies whose data to show, it does not grant power over it. Everything it can read is going to be on your public website anyway.

The one thing a copied key can do is spend your request quota: each key has its own limit of 600 requests per minute (see Rate limits). If someone starts doing that, rotate the key — that takes a couple of minutes.

Keeping the key in environment variables

The key is public, so why bother with env vars? Because keys change — a rotated key should be a config change, not a code change. Most frameworks also need a prefix before they let a variable into browser code:

.envbash
# Next.js — Server Components, Route Handlers (never sent to the browser)
CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40

# Next.js — Client Components (inlined into the bundle at build time)
NEXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40

# Vite (React, Vue, Svelte…) — inlined at build time
VITE_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40

# Nuxt — overrides runtimeConfig.public.crmKey
NUXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
// app/page.tsx — a Server Component
export default async function Home() {
  const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
    headers: { 'x-crm-key': process.env.CRM_KEY! },
    next: { revalidate: 60 },
  });
  const page = await res.json();

  return <h1>{page.content.hero.content.hero_title.content.en}</h1>;
}

Revoking and rotating keys

Any key can be revoked in the CRM in the same Settings → Content API keys list. A revoked key stops working: every request with it gets 401 invalid_key. To swap keys without a single failed request, overlap them:

  1. Create a new key

    Leave the old one alone for now — both keep working side by side.
  2. Deploy with the new key

    Update the environment variable everywhere the old key was used, rebuild if your framework inlines it, and deploy.
  3. Check that the site still loads content

    Open a couple of pages, look at the logs. No 401s? Good.
  4. Revoke the old key

    Now it is safe to pull the plug.

Key errors

StatusBodyWhat happened
401{"message":"invalid_key"}The key is missing, malformed (not pk_ + 32 hex) or revoked.
429{"message":"rate_limit_exceeded"}Too many requests — or too many bad keys from your IP this minute (see below).

The bad-key lockout

To keep key guessing pointless, we count requests with invalid keys per IP. More than 20 bad-key requests in a minute from one IP, and that IP gets 429 for the rest of the minute — even with a valid key. There are no Retry-After headers; the window resets at the start of the next minute.

The classic way to trip it by accident: you revoke an old key, but one server or a forgotten cron job still uses it. Server-side rendering keeps firing 401s, and a minute later the whole server is locked out — new key included. So:

Don't retry a 401js
const res = await fetch(url, { headers: { 'x-crm-key': process.env.CRM_KEY } });

if (res.status === 401) {
  // A wrong or revoked key won't fix itself. Retrying only burns
  // through the bad-key budget and gets this IP locked out.
  throw new Error('Content API: invalid site key, check CRM_KEY');
}

if (res.status === 429) {
  // Back off until the next minute (about 60 s, plus a little jitter).
}

All other codes are on the Errors page.

Security FAQ