> ## Documentation Index
> Fetch the complete documentation index at: https://cal.id/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cal ID API Error Codes

> Every error.code the Cal ID API returns — what each one means, the details payload it carries, and whether retrying will help.

When a request fails, the Cal ID API returns a JSON body with a machine-readable `error.code` and a human-readable `message`. Branch your error handling on `error.code` rather than on the HTTP status alone — the same status can carry different codes, and the codes tell you whether the fix is in your request or in your data.

```json theme={null}
{
  "success": false,
  "message": "A human-readable explanation of what went wrong",
  "error": { "code": "UNAUTHORIZED", "message": "Invalid API key" }
}
```

## The codes

Six distinct `error.code` values were observed in testing.

| `error.code`         | Typical status | What it means                                                                                                                                                                                                 | What to do                                                                                                                                                                                                                                      |
| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FST_ERR_VALIDATION` | `400`          | The request failed the endpoint's schema before any business logic ran — a missing required field, a wrong type, or a query parameter outside its allowed range. Example: `querystring/limit must be <= 100`. | Read `message`; it names the offending field using a JSON-pointer-style path. Fix the request. There is no `details` array to inspect. Retrying unchanged fails identically.                                                                    |
| `BAD_REQUEST`        | `400`          | A schema violation reported with the underlying validator's own output attached.                                                                                                                              | Read `error.details[]` for the per-field failures, then fix the request. Deterministic — do not retry.                                                                                                                                          |
| `VALIDATION_ERROR`   | `422`          | The request parsed and matched the schema, but failed a semantic check. Observed on `GET /slots/` when only `eventTypeSlug` is supplied without the companion parameters that make it resolvable.             | Read `error.details[]`, which lists each failure as `{ path, message }`. Supply the missing or conflicting parameters. Deterministic — do not retry.                                                                                            |
| `UNAUTHORIZED`       | `401`          | The API key is missing, malformed, expired, or revoked.                                                                                                                                                       | Check the `Authorization: Bearer calid_…` header. There is no query-parameter fallback — a request without the header is rejected regardless of the URL. See [Get your API key](/docs/developers/api-key).                                           |
| `NOT_FOUND`          | `404`          | The record you asked for does not exist, or your key cannot see it.                                                                                                                                           | Verify the ID. Note that a permission failure often surfaces here rather than as a `403`.                                                                                                                                                       |
| `INTERNAL_ERROR`     | `500`          | A failure inside the API. On Cal ID this is **usually caused by the request**, not by an outage — most often a scheduling rule refusing the operation.                                                        | Read `message` first. If it names a domain error such as `no_available_users_found_error`, the request is unsatisfiable and no retry will help. Only escalate as an outage if `message` is generic and the same call succeeded moments earlier. |

<Warning>
  Do not build a retry-with-backoff policy around the `5xx` class. On this API, `500` is usually a deterministic rejection of your request. The only status worth retrying automatically is `429` — see [Rate limits](/docs/developers/rate-limits).
</Warning>

## The three validation codes carry different `details`

`FST_ERR_VALIDATION`, `BAD_REQUEST`, and `VALIDATION_ERROR` all mean "your request is wrong", but they are not interchangeable in code. Each attaches a different payload, so a single parser written against one of them will break on the other two.

<CodeGroup>
  ```json FST_ERR_VALIDATION — no details theme={null}
  {
    "success": false,
    "message": "querystring/limit must be <= 100",
    "error": { "code": "FST_ERR_VALIDATION" }
  }
  ```

  ```json BAD_REQUEST — ajv details array theme={null}
  {
    "success": false,
    "message": "Validation failed",
    "error": {
      "code": "BAD_REQUEST",
      "details": [ ]
    }
  }
  ```

  ```json VALIDATION_ERROR — path and message per failure theme={null}
  {
    "success": false,
    "message": "Validation failed",
    "error": {
      "code": "VALIDATION_ERROR",
      "details": [
        { "path": "eventTypeSlug", "message": "…" }
      ]
    }
  }
  ```
</CodeGroup>

| Code                 | `details`                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `FST_ERR_VALIDATION` | **None.** The whole diagnosis is in the top-level `message`.                                                              |
| `BAD_REQUEST`        | An array of raw [ajv](https://ajv.js.org) validation objects. Their shape is ajv's, not Cal ID's — read them defensively. |
| `VALIDATION_ERROR`   | An array of `{ path, message }` objects, one per failing field.                                                           |

<Note>
  Write your error formatter to check for `error.details` before reading it, and to fall back to `error.message` and then the top-level `message`. That covers all three codes without special-casing any of them.
</Note>

<Warning>
  `VALIDATION_ERROR` was observed on a `422`. Whether it can also accompany a `400` on other endpoints has not been established — match on `error.code`, not on the status code, so your handling is correct either way.
</Warning>

## `no_available_users_found_error`

This is not an `error.code`. It is a domain message that arrives in the top-level `message` field alongside `error.code: "INTERNAL_ERROR"` and a `500` status.

```json theme={null}
{
  "success": false,
  "message": "no_available_users_found_error",
  "error": { "code": "INTERNAL_ERROR" }
}
```

It means the booking you asked for is not permitted by the scheduling rules — practically always because `start` was not a time that `GET /slots/` returned. Availability is the product of schedules, buffers, minimum notice, booking limits, connected-calendar busy times, and timezone rules; a time that looks free on a calendar is frequently not bookable.

<Warning>
  This is the most common `500` on the API, and it is **entirely deterministic**. Retrying with the same `start` will fail every time. Fetch fresh slots from `GET /slots/`, pick a time it actually offered, and send that. See the [Quickstart](/docs/developers/quickstart) for the full sequence.
</Warning>

`POST /slots/reserve` does **not** protect you from this. Reserving a time that `GET /slots/` never offered returns success; the failure surfaces later, at `POST /booking/`.

## Two error shapes that are not the envelope

Not every failure comes back in the `{ success, message, error }` envelope. Two do not, and both will break a client that assumes it.

### Unknown route or wrong HTTP method

Mistype a path, or call a real path with the wrong verb, and the response comes from the router rather than the API. There is **no `success` field**, and `error` is a plain string instead of an object.

```json theme={null}
{
  "message": "Route GET:/user/me not found",
  "error": "Not Found",
  "statusCode": 404
}
```

<Warning>
  Code written as `if (!body.success)` treats this as a success, and code written as `body.error.code` throws a type error on a string. Either way you lose the real diagnosis, which is simply that the URL is wrong. Check the HTTP status code before you read the body.
</Warning>

### Rate limiting

A `429` returns its own shape, with a `retryAfter` in seconds and no `error.code`.

```json theme={null}
{
  "error": "Rate limit exceeded",
  "message": "Too many requests. Please try again later.",
  "retryAfter": 30
}
```

Unlike the codes above, this one **is** transient. Wait `retryAfter` seconds, then back off exponentially. See [Rate limits](/docs/developers/rate-limits).

## Handling errors in practice

<Steps>
  <Step title="Branch on the HTTP status first">
    It is the only field guaranteed to be present. It tells you immediately whether you are in envelope territory (`400`, `401`, `422`, `500` from the API) or not (`404` from the router, `429` from the limiter).
  </Step>

  <Step title="Then read error.code">
    Use it to decide whose problem this is: a `4xx` code means fix the request; `INTERNAL_ERROR` means read `message` before assuming anything.
  </Step>

  <Step title="Surface message, not code, to humans">
    The `message` field names the offending field or the domain rule. `FST_ERR_VALIDATION` on its own tells a user nothing.
  </Step>

  <Step title="Retry only 429">
    Everything else in this list is deterministic. A retry loop on `500` will burn your rate-limit allowance producing the identical failure.
  </Step>
</Steps>

<Note>
  These six codes are the ones observed in live testing. They are not guaranteed to be the complete set — always keep a default branch that logs the raw body for any `error.code` you do not recognise.
</Note>

## Debugging resources

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/docs/developers/quickstart">
    From a fresh API key to a confirmed booking in five requests.
  </Card>

  <Card title="API integration" icon="plug" href="/docs/developers/api-integration">
    Envelopes, pagination, and the behaviours that cause most first-build bugs.
  </Card>

  <Card title="Rate limits" icon="gauge-high" href="/docs/developers/rate-limits">
    The headers, the 429 body, and how to back off cleanly.
  </Card>

  <Card title="API Reference" icon="code" href="/docs/api-reference/introduction">
    Every endpoint, with schemas and a live "Try it" playground.
  </Card>
</CardGroup>


## Related topics

- [Cal ID API Integration](/docs/developers/api-integration.md)
- [Cal ID API Quickstart](/docs/developers/quickstart.md)
- [Cal ID API Rate Limits](/docs/developers/rate-limits.md)
- [How to Get Your Cal ID API Key](/docs/developers/api-key.md)
- [Cal ID Scheduling API Reference](/docs/api-reference/introduction.md)
