Diil Docs
  1. Docs
  2. Guides

Errors: every code and what to do about it

Updated:

Every error the Content API can send you, in one place: what the body looks like, why it usually happens and how to fix it. The API is read-only and fairly small, so the list is short — and every error comes with a machine-readable message, so your code never has to guess.

All errors at a glance

StatusmessageIn one sentence
400invalid_markerThe marker in the URL is not a valid marker.
400invalid_langA language code in lang is malformed.
400unknown_langA language code is fine, but the site has no such active language.
400too_many_langsMore than 50 codes in lang.
401invalid_keyThe site key is missing, malformed or revoked.
404page_not_foundNo page with this marker.
404section_not_foundNo section with this marker.
404block_not_foundNo block with this marker (in this section).
404post_not_foundNo visible blog post with this slug.
404Cannot GET /v1/…The URL itself does not exist in the API.
405method_not_allowedAnything other than GET or HEAD.
429rate_limit_exceededToo many requests this minute.
5xx—Something broke on our side.

What an error looks like

Every error body is JSON with a message field. Some errors add a few helpful fields — for example, unknown_lang tells you which languages the site actually has.

A typical error responsehttp
HTTP/1.1 404 Not Found
Content-Type: application/json
Cache-Control: no-store

{"message":"page_not_found"}

Errors are always sent with Cache-Control: no-store, so a 404 for a page that an editor is creating right now will not get stuck in a browser or CDN cache. Fix the cause, and the next request gets a normal answer.

400 Bad Request

The request itself is wrong. Retrying will not help — fix the URL.

invalid_marker

400json
{"message":"invalid_marker"}

Markers of pages, sections and blocks must match ^[A-Za-z0-9_]{2,40}$: Latin letters, digits and underscores, 2 to 40 characters.

Typical causes:

  • A hyphen or a dot: /v1/pages/about-us — markers use underscores, about_us.
  • A path instead of a marker: the page's href (/about) is not its marker (about).
  • A single character, more than 40 characters, spaces or non-Latin letters.
  • A URL segment from a visitor passed straight to the API — someone typed /über-uns in the address bar.

How to fix: copy the marker from the CRM. If your routes use hyphens, map them to markers in your router. And when a marker comes from a visitor's URL, treat invalid_marker exactly like a 404: that page simply does not exist.

invalid_lang

400json
{"message":"invalid_lang","lang":"EN"}

Each code in lang must look like ^[a-z]{2}(-[A-Za-z]{2,4})?$: two lowercase letters, optionally followed by a region like pt-BR. The check is case-sensitive. The lang field echoes the value that did not pass.

Typical causes:

  • Uppercase: EN instead of en.
  • An underscore locale from a server or an i18n library: en_US.
  • Three-letter codes like eng, or a full name like english.

How to fix: use the exact codes the site has — they come from GET /v1/langs. Normalising whatever your framework gives you to one of those codes is the most reliable approach.

unknown_lang

400json
{
  "message": "unknown_lang",
  "lang": "fr",
  "unknown": ["fr"],
  "available": ["en", "de"]
}

The code is well-formed, but it is not an active language of this site. The body is genuinely useful: unknown lists every requested code that failed, available lists the ones you can use.

Typical causes:

  • An editor switched a language off in the CRM, and your code still asks for it by name.
  • The browser locale passed as is: en-US is a valid format, but the site has en.
  • A neighbour's code: ua where the site uses uk, cz where it uses cs.
  • The key belongs to a different site with a different set of languages.

How to fix: build your language list from GET /v1/langs instead of hard-coding it, or fall back to the first code from available. If you need every language anyway, just leave lang out. More in Languages & fallbacks.

too_many_langs

400json
{"message":"too_many_langs","max":50}

You can ask for at most 50 codes in one request. Hitting this usually means the list is built from something unbounded — a header, user input, a loop that keeps appending.

How to fix: to get all active languages, omit lang entirely — that is exactly what it does by default.

401 invalid_key

401json
{"message":"invalid_key"}

The API did not accept the site key. A valid key looks like pk_ followed by 32 lowercase hex characters and is sent in the x-crm-key header (or as ?key=).

Typical causes:

  • The header is missing — most often because the environment variable is empty in this environment.
  • In the browser, the variable never made it into the bundle (in Next.js only NEXT_PUBLIC_* variables do), so the header literally says undefined.
  • Quotes, spaces or a trailing newline copied into .env along with the key.
  • The key was revoked in the CRM.
  • The key was created a moment ago. Usually it works instantly; in the worst case it can take a few minutes to be recognised. The same goes for revocation.

How to fix: check the key in the CRM under Settings → Content API keys and test it with curl. Details on the Site keys page.

Is this key alive?bash
curl -i "https://back.sitecog.com/content/v1/langs" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

404 Not Found: no such content

The URL is fine, the key is fine, but there is nothing at that address. The message tells you what exactly is missing:

messageEndpointUsual suspects
page_not_found/v1/pages/:markerA typo or wrong case (Home ≠ home), a page renamed or deleted in the CRM, a key of another site.
section_not_found/v1/sections/:markerSame as above. Note that hidden sections (show: false) are still returned — a 404 really means the section is not there.
block_not_found/v1/blocks/:markerA typo, or the block lives in a different section than the one you passed in ?section=.
post_not_found/v1/blog/:slugThe post is a draft, is scheduled for the future, or the slug is invalid (only lowercase a–z, digits and hyphens, up to 120 characters — an invalid slug gives 404, not 400).
404json
{"message":"page_not_found"}

How to fix: if the marker is hard-coded in your templates, check it against the CRM. If it comes from a visitor's URL, this is not a bug at all — it is a regular 404, so show your “page not found” page (in Next.js, call notFound()).

Unknown route

If the URL does not match any endpoint, you get the framework's standard 404, which looks different from the content 404s above:

404json
{
  "message": "Cannot GET /v1/page/home",
  "error": "Not Found",
  "statusCode": 404
}

You can tell it apart by the error and statusCode fields and by the Cannot GET … message. It is always a bug in your code, never missing content.

Typical causes:

  • A singular instead of a plural: /v1/page/home, /v1/block/phone.
  • A forgotten /v1 in the path.
  • Extra segments: /v1/pages/home/hero — sections have their own endpoint, /v1/sections/hero.

How to fix: compare the URL with the API reference. Keeping the base URL in one constant (https://back.sitecog.com/content) and building paths in one helper prevents most of these.

405 method_not_allowed

405json
{"message":"method_not_allowed"}

The Content API is read-only: it answers GET and HEAD. POST, PUT, PATCH and DELETE get a 405.

Typical causes: an HTTP client defaulting to POST, a form whose action points at the API, or the hope of saving content through it. Content is edited in the CRM or right on the site in Live mode — the API only reads.

429 rate_limit_exceeded

429json
{"message":"rate_limit_exceeded"}

Too many requests in the current minute — from your IP (300), with your key (600), or too many bad-key attempts from your IP (20). There is no Retry-After header: wait for the next minute, with a little jitter. The limits, the maths and a ready-made retry helper are on the Rate limits page.

5xx: our side

A 500, 502, 503 or 504 means something went wrong on our side, not in your code. These are the errors worth retrying: a short exponential backoff with jitter (example), and if it still fails, show the last good copy you have cached. A site that keeps serving yesterday's text during a hiccup is much better than a site that shows a stack trace.

Network errors and CORS

Sometimes there is no response at all — fetch rejects with a TypeError (“Failed to fetch”, “fetch failed”). Causes:

  • DNS trouble, no connection, a firewall on your server, a request that hangs. Set a timeout: AbortSignal.timeout(10_000).
  • CORS in the browser. The API allows any origin, methods GET and OPTIONS, and request headers x-crm-key, content-type and if-none-match. Add any other custom header (an Authorization, a tracing header from your HTTP client) and the browser blocks the request before it is sent — your code sees it as a network error. Check the console: the browser tells you which header it did not like.

A robust error handler in TypeScript

Put all of the above into one small module: every non-2xx response and every network failure becomes a typed ContentApiError with the status, the machine-readable code and the extra fields. The rest of your code just checks err.code.

lib/content-api.tsts
const API = 'https://back.sitecog.com/content';

export type ContentErrorCode =
  | 'invalid_marker' | 'invalid_lang' | 'unknown_lang' | 'too_many_langs'
  | 'invalid_key'
  | 'page_not_found' | 'section_not_found' | 'block_not_found' | 'post_not_found'
  | 'method_not_allowed' | 'rate_limit_exceeded'
  | 'unknown_route'   // 404 "Cannot GET …" — the URL is wrong
  | 'server_error'    // 5xx
  | 'network_error'   // no response at all
  | 'unexpected';     // anything we have not seen before

const KNOWN = new Set<string>([
  'invalid_marker', 'invalid_lang', 'unknown_lang', 'too_many_langs', 'invalid_key',
  'page_not_found', 'section_not_found', 'block_not_found', 'post_not_found',
  'method_not_allowed', 'rate_limit_exceeded',
]);

type ErrorBody = {
  message?: string;
  lang?: string;
  unknown?: string[];
  available?: string[];
  max?: number;
};

export class ContentApiError extends Error {
  readonly status: number;          // HTTP status, 0 when there was no response
  readonly code: ContentErrorCode;
  readonly lang?: string;           // invalid_lang, unknown_lang
  readonly unknown?: string[];      // unknown_lang: the codes that failed
  readonly available?: string[];    // unknown_lang: the codes you can use
  readonly max?: number;            // too_many_langs

  constructor(status: number, code: ContentErrorCode, body: ErrorBody = {}, cause?: unknown) {
    super(`Content API ${status || 'network'}: ${body.message ?? code}`, { cause });
    this.name = 'ContentApiError';
    this.status = status;
    this.code = code;
    this.lang = body.lang;
    this.unknown = body.unknown;
    this.available = body.available;
    this.max = body.max;
  }

  /** Worth trying again later: network trouble, rate limit, our 5xx */
  get retryable() {
    return this.status === 0 || this.status === 429 || this.status >= 500;
  }

  /** The content does not exist — show your 404 page */
  get notFound() {
    return this.code.endsWith('_not_found') || this.code === 'invalid_marker';
  }
}

function toCode(status: number, message?: string): ContentErrorCode {
  if (status >= 500) return 'server_error';
  if (message && KNOWN.has(message)) return message as ContentErrorCode;
  if (status === 404 && message?.startsWith('Cannot ')) return 'unknown_route';
  return 'unexpected';
}

export async function contentFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
  let res: Response;
  try {
    res = await fetch(API + path, {
      ...init,
      headers: { ...(init.headers as Record<string, string>), 'x-crm-key': process.env.CRM_KEY! },
      signal: init.signal ?? AbortSignal.timeout(10_000),
    });
  } catch (cause) {
    throw new ContentApiError(0, 'network_error', {}, cause);
  }

  if (res.ok) return (await res.json()) as T;

  // Error bodies are JSON, but a proxy on the way might answer with HTML — be careful
  const body: ErrorBody = await res.json().catch(() => ({}));
  throw new ContentApiError(res.status, toCode(res.status, body.message), body);
}

And here is how it reads in real code:

// app/[lang]/[marker]/page.tsx
import { notFound, redirect } from 'next/navigation';
import { contentFetch, ContentApiError } from '@/lib/content-api';

type Props = { params: Promise<{ lang: string; marker: string }> };

export default async function Page({ params }: Props) {
  const { lang, marker } = await params;

  try {
    const page = await contentFetch<PageData>(`/v1/pages/${marker}?lang=${lang}`, {
      next: { revalidate: 60 },
    });
    return <PageView page={page} lang={lang} />;
  } catch (err) {
    if (err instanceof ContentApiError) {
      if (err.notFound) notFound();
      if (err.code === 'unknown_lang' && err.available?.length) {
        redirect(`/${err.available[0]}/${marker}`);
      }
      console.error(err.code, err.status, err.message); // invalid_key, unknown_route… — a bug to fix
    }
    throw err; // error.tsx shows "something went wrong"
  }
}