---
title: Troubleshooting
description: The failures people actually hit, and what each one is really telling you.
---

## "It returns an empty list and I know there is content"

An empty list means your query was **valid** and matched nothing. Three usual
causes:

1. **Nothing is published.** `/v1` serves published content only. A draft is
   invisible here and visible in the dashboard, which is exactly how it should
   look while somebody is still writing.
2. **You are on the wrong release.** Custom content is release-scoped. Without
   `?release=` you are reading the default release, which may not be the one
   your colleague just edited. The built-in modules are release-*independent*,
   so this never explains a missing blog or knowledge base article. See
   [Releases](/concepts/releases).
3. **The filter matched nothing.** `?tag=setup` with no such tag is an empty
   list, not an error, because a tag is free-form and "no articles carry this"
   is a real answer.

> **INFO: An empty list is never a typo**
>
> Naming something that does not exist, such as an unknown category, section,
> field or sort key, is a `4xx` that names the real ones, not an empty list. So
> if you got `200` with `[]`, the names in your query were all correct.

## "I get 401 and the key is right"

`401` means the key did not resolve. Call `GET /v1/ping`, which confirms the
key and names its project. That separates "wrong key" from "wrong request".

If ping succeeds and your real request still fails, you are not looking at an
authentication problem.

## "It works in curl and fails in the browser"

`origin_not_allowed`. A publishable key is locked to the origins listed on it,
and `curl` does not send an `Origin` header, so it is never checked there.

Add the origin to the key in the dashboard, with scheme, host and port exactly
as the browser sends it. `www.` is ignored when matching, so
`https://example.com` covers `https://www.example.com`. `http://localhost:3000`
and `http://localhost:5173` are different origins.

## "My image URLs stopped working"

Your project's bucket is **private**, so image URLs are presigned and expire.
The object carries `url_expires_at` saying exactly when.

That is fine for a page fetching at runtime and fatal for a static build, which
bakes the URL into HTML that outlives it. Pass `?media_urls=permanent` and the
API will **refuse** rather than hand you links that die, turning a silent
breakage weeks later into an error at build time. If you need permanent URLs,
make the bucket public. See [Media](/concepts/media).

## "I get 429 and I am not making many requests"

Check which budget. The header names it: `X-RateLimit-Remaining-read`,
`-write` or `-vote`. Budgets are separate, so reading all day never uses up
your allowance to vote.

Two things that surprise people:

- A **publishable** key is counted per key *and* per address. If your server is
  making requests with a publishable key, every one of them shares a single
  address budget. Use a secret key server-side.
- `Retry-After` is when the refusal **lifts**, not when the counting window
  rolls. Wait exactly that long. Retrying sooner earns another 429.

See [Rate limits](/api/rate-limits).

## "`where[…]` seems to be ignored"

Two possibilities, and the response tells you which:

- **A `400`** naming the fields that exist means you used a field key that is
  not on the type. `response_shape` in `/v1/introspection` lists them.
- **A `400`** naming operators means the operator does not apply to that field
  type. `contains` on a boolean is refused rather than silently matching
  nothing, because answering it with "0 results" would hide the mistake.

`where` only exists on `/v1/content`. The built-in modules take named filters
instead, because their schema is fixed: `category`, `author`, `tag` and `q`
for the blog; `collection`, `section` and `q` for the knowledge base.

## "Sorting changed how paging works"

It did, and the response says so. A cursor encodes a position in the *default*
order, so asking `/v1/content` for a `sort` switches it to offset paging:
`next_cursor` goes null and `next_offset` fills in.

Read both keys and pass back whichever is set. That one loop works everywhere.

## "The knowledge base ignores `?sort=`"

There is deliberately no `sort` anywhere in the knowledge base. Every listing
comes back in the order somebody dragged it into: collection, then section,
then the manual order inside it. A help centre in date order is not a help
centre.

If you need another order, sort client-side, knowing you are overriding an
editorial decision.

## "A slug I know exists returns 404"

Read the `code`:

- `kb_article_not_published` / `blog_article_not_published`: it exists and is
  a draft. Publish it.
- `kb_article_archived`: it was retired on purpose. Restore it, or remove the
  link.
- `unknown_kb_article` / `unknown_blog_article`: no such slug. Check
  `did_you_mean` in the error; it is usually a typo.

## Still stuck

Every response carries `X-Request-Id`, and every error repeats it as
`request_id`. Quote it, and it leads straight to the log lines for that exact
request.