# Fetch API Status Codes and Caching

> Source: https://agilitycms.com/docs/developers/fetch-api-status-codes-and-caching

This page lists the HTTP status codes the Content Fetch API and the GraphQL API return, what each one means for your code, and how responses are cached. Use it when you build error handling, retries or caching around Agility content.

For how to authenticate and make your first request, see [Content Fetch API](/docs/developers/content-fetch-api).

## Status codes at a glance

| Code | When you get it | What to do |
| --- | --- | --- |
| `200 OK` | The request worked. For a content list this includes a list with no items in it. | Use the response. |
| `400 Bad Request` | The `filter` parameter has an error (REST or GraphQL), or the API type in the URL is not `fetch` or `preview`. | Fix the request. Retrying it unchanged will fail again. |
| `401 Unauthorized` | The `APIKey` header is missing, or the key is not valid for this instance. | Check the key, its type (fetch or preview) and its expiry under **Settings > API Keys**. |
| `404 Not Found` | The item, page or other resource you asked for was not found. On live endpoints this includes content that has not been published. | Treat as "not there". Don't retry in a loop. |
| `408 Request Timeout` | The request took longer than 30 seconds to process. | Wait at least 30 seconds, then retry. The 408 response is cached for 30 seconds (see below). |
| `429 Too Many Requests` | You sent more than 10 uncached requests in one second. | Slow down and retry with back-off. Cache responses on your side. |

## 400: filter errors and bad API types

Since August 2023, a request whose `filter` parameter contains an error returns **400 Bad Request** instead of 500. This applies to both the REST API and GraphQL. A 400 means the request itself is wrong, so log the response body and fix the filter rather than retrying.

For the filter syntax and operators, see [GraphQL & Rest API Filtering](/docs/developers/graphql-operators).

The API also returns 400 when the API type segment of the URL is something other than `fetch` or `preview`. For example, this request (sent on 2026-10-03):

```bash
curl -i "https://api.aglty.io/{guid}/blah/en-us/list/posts" -H "APIKey: {your-key}"
```

returned:

```text
HTTP/2 400
cache-control: private, no-store

Invalid API type (must be fetch or preview)
```

## 401: missing or invalid API key

Every Fetch API and GraphQL request needs an `APIKey` header. Observed on 2026-10-03:

- No `APIKey` header: `401` with the body `Missing API Key`.
- A key that is not valid for the instance: `401` with the body `Invalid API Key for {guid}`.

Both responses carried `cache-control: private, no-store`, so a fixed key takes effect on the next request. The GraphQL endpoint (`POST https://api.aglty.io/v1/{guid}/{fetch|preview}/{locale}/graphql`) returned the same 401 messages.

Remember that keys have a type. A **fetch** key reads published content from the `fetch` endpoints. A **preview** key reads the latest saved (staging) content from the `preview` endpoints. The key must support the API type in the URL. Keys can also have an expiry date, so check the expiry when a key that used to work starts returning 401.

## 404 and empty lists

Asking for a single item, page or gallery that isn't there returns **404**. On the live (`fetch`) endpoints, "isn't there" includes content that exists but has never been published, so a 404 for an item you can see in the CMS usually means it hasn't been published yet. The same request with a preview key and the `preview` endpoint will return the staging version.

Content **lists** behave differently. Since July 2024, an empty content list always returns an empty result, not a 404. Before that change, a list where no item had ever been published could return 404, while a list whose items had all been unpublished or deleted returned an empty result. Now both cases look the same:

```json
{
  "items": [],
  "totalCount": 0
}
```

So for lists, check `items.length` (or `totalCount`) rather than catching a 404. Older code that treats a 404 from a list endpoint as "empty" is harmless, but for lists with no published items it no longer runs.

> [!NOTE]
> List endpoints return 10 items by default. Pass `take` (maximum 250) and page with `skip` when a list can be longer. A short page is not an error: it means you reached the end.

## 408: timeouts

When a request takes longer than 30 seconds to process, the API responds with **408 Request Timeout**. Since August 2023 the `cache-control` header on a 408 response sets a **30 second** cache lifetime (it used to be 24 hours).

In practice this means:

- A retry within about 30 seconds may get the same cached 408 back. Wait at least that long before retrying the identical URL.
- To make a timeout less likely, ask for less: lower `ContentLinkDepth`, avoid `ExpandAllContentLinks=true` on large lists, use `fields` to return only the fields you need, and page with `take` and `skip`.

## 429: rate limits

The Fetch API allows **10 uncached requests per second**. Above that it responds with **429 Too Many Requests**.

Responses served from the CDN cache do **not** count towards this limit. The limit is about requests that reach the API itself, for example the first request for a URL, or requests for many different URLs in a burst.

Ways to stay under it:

- **Cache on your side.** Store responses in your framework's data cache and invalidate them from a [webhook](/docs/developers/webhooks) when content is published, instead of fetching on every page view.
- **Limit build concurrency.** Static builds that generate many pages at once can send a burst of requests. See [Troubleshooting fetch failed errors in Next.js](/docs/nextjs/troubleshooting-fetch-failed-errors-in-next-js) for how to lower concurrency.
- **Fetch less often.** Ask for lists with `take` up to 250 instead of many small pages, and request linked content with `ContentLinkDepth` rather than one call per linked item.
- **Keep a local copy.** If you read the same content constantly, the [Content Sync](/docs/developers/content-sync-explained) approach lets you query your own store instead of the API.
- **Back off on 429.** Wait, then retry with an increasing delay.

## How responses are cached

The Fetch API and GraphQL API are served through a CDN. Each request is either:

- **Cached:** the CDN already holds a response for that exact URL and returns it. This is fast and does not count towards the rate limit.
- **Uncached:** the request goes through to the API. This is slower and counts towards the 10 requests per second limit.

When content is published, Agility invalidates the affected cached responses, so the next request gets the new version. See [Content Delivery & CDN Architecture](/docs/developers/content-delivery-and-cdn-architecture) for the full picture, including the separate CDN in front of your own site.

Error responses we observed (400 and 401) carried `cache-control: private, no-store`, so they are not reused. The 408 response is the exception described above: it is cached for 30 seconds.

### Your own cache is a separate layer

Agility's CDN caches API responses. Your website or app usually has its own cache as well (for example, the Next.js data cache, or the CDN of your host). Publishing in Agility does not clear that layer for you. Use webhooks to tell your app what changed, then revalidate the matching pages or cache entries.

## Preview vs live requests

| | Live (`fetch`) | Preview (`preview`) |
| --- | --- | --- |
| Key type | Fetch key | Preview key |
| Returns | Published content | The latest saved content, including staging changes |
| Typical use | Production site | Preview and editing environments |

Use the preview key only on the server or in a preview environment. It reads unpublished content.

## Related

- [Content Fetch API](/docs/developers/content-fetch-api)
- [Field Types and What the APIs Return](/docs/developers/field-types-api-reference)
- [Content Sync Explained](/docs/developers/content-sync-explained)
- [GraphQL API](/docs/developers/graphql-api)
- [Webhooks](/docs/developers/webhooks)
