# API error shapes

Errors come back as `{"error": {"code": …, "message": …}}` with a matching HTTP status. Two codes exist today — `not_found` at `404` and `rate_limited` at `429`. A parameter outside its documented bounds is rejected earlier, by request validation, and that one response uses the framework's own shape rather than this envelope.

- **Status:** Available
- **Audience:** developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/developers/api-errors/

## The envelope

```json
{"error": {"code": "not_found",
           "message": "No public page for '/docs/nope/'. Search first: /api/public/docs/search?q=…"}}
```

| HTTP | `code` | Cause | What to do |
|---|---|---|---|
| `404` | `not_found` | No page matches the `ref` given to `/docs/page` | Search first and use a `url` or `id` from the result |
| `404` | `not_found` | No manifest by that name at `/docs/manifest/{name}` | Read the manifest list rather than guessing a name |
| `429` | `rate_limited` | More than 240 requests in the last minute from one address | Wait, then retry with a widening delay |

The `message` is written for a person reading a log and often names the next call to make. The `code` is the part to branch on: it is a short string, it does not change wording between releases, and it is what the published description documents.

## The response that is not this shape

Every parameter has bounds — `q` is 2 to 200 characters, `limit` on search is 1 to 25, `limit` on listing is 1 to 1,000, `ref` is at most 300 characters. A request that violates one is rejected by request validation *before* the handler runs, so it never reaches the code that builds the envelope. That response carries the framework's own `detail` array naming the offending parameter, with status `422`, and the published description's `400` entry describes the malformed-request case rather than this one.

It is worth handling deliberately rather than discovering in production: a client that only knows the `error` envelope will fail to read the most informative failure it can get, which is the one that names the parameter you got wrong.

```http
GET /api/public/docs/search?q=a HTTP/1.1   -> 422 (q is shorter than 2)
GET /api/public/docs/search?q=email&limit=99 -> 422 (limit exceeds 25)
GET /api/public/docs/page?ref=/docs/nope/    -> 404 {"error": …}
GET /api/public/docs/status                  -> 200
```

## Server-side failure

An unhandled fault is recorded on the server and answered with a deliberately vague body: no stack trace, no internal path and no identifier that would say anything about another workspace. That is the correct behaviour and it does mean a `5xx` gives you nothing to act on beyond *retry once, then stop*.

Distinguish it from the errors above by intent. A `404` and a `422` are your request being wrong and will not improve on retry. A `429` is timing and will. A `5xx` is the service, and a client that retries it in a tight loop turns one fault into a rate-limit ban on top of it.

## A retry policy that fits this API

1. Classify on the status first, then on `error.code` if there is one.
   - Result: You get a decision without parsing prose, and one branch for the validation shape that has no code.
2. Retry only `429` and `5xx`, with a widening delay and a cap.
   - Result: The rate-limit window is sixty seconds, so a few seconds of waiting usually clears it.
3. On `404` from `/docs/page`, fall back to a search rather than retrying the same reference.
   - Result: The reference was wrong, and the error message says so with the search URL to use.
4. Log the `code` and the request path, not the whole body.
   - Result: The codes are a small closed set and count cleanly; messages are prose and do not.

## Questions

### Are there more codes than these two?

These are the ones the handlers produce today. The published description also names bad-request and rate-limited responses at the schema level, all using the same envelope, so a client that branches on `error.code` with a default branch will not break if another is added.

### Does an error response carry the cache headers?

No. The five-minute cache header is attached to successful responses only, so a `404` or a `429` is not cached on your behalf and a corrected request is answered fresh.

### What do the MCP and A2A endpoints return instead?

JSON-RPC error objects, which are a different shape with numeric codes. See [MCP error shapes](/developers/mcp-errors/) and [Interoperating over A2A](/developers/a2a-integration/).

## Related

- [The public API](https://connectbyjbrh.com/developers/public-api/)
- [API rate limits](https://connectbyjbrh.com/developers/api-rate-limits/)
- [Designing a resilient client](https://connectbyjbrh.com/developers/errors-and-retries/)
- [MCP error shapes](https://connectbyjbrh.com/developers/mcp-errors/)
- [The OpenAPI description](https://connectbyjbrh.com/developers/openapi-description/)

## What this page is based on

- `backend/app/public_developer_api.py` — _error, _guard and the handlers
- `webapp/developers/openapi.yaml` — the Error schema and response components
- `backend/app/main.py` — the unhandled-exception handler
