# Errors

Failed calls use `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html)).

```json
{
  "type": "https://ipa.raymati.com/docs/errors#bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "request must be a JSON object",
  "instance": "/v1/tutor"
}
```

`type` is a section of this page. `detail` is one sentence. Success JSON is not a problem document.

## Bad request

Status 400. The body was not a JSON object, the target was empty, the audio was not a wav, or neither `ipa` nor audio was usable. `detail` is `request must be a JSON object`, `target must be a Greek word or verse`, or `send ipa, audio_base64, or audio_url`, among the scorer's own sentences. Inline audio over 256 KiB says to call `create_upload`.

## Not found

Status 404. `detail` is `not found`.

## Method not allowed

Status 405. `Allow` names the methods that path accepts. `GET /mcp` is 405 because this server does not open an SSE stream.

## Not acceptable

Status 406. `POST /mcp` received an `Accept` header that allows neither `application/json` nor `text/event-stream`.

HTML guides are different. `Accept: text/markdown` is a 200 Markdown body (draft-consolidated-content-01), not a 406. `*/*` stays HTML.

## Too many requests

Status 429. `detail` is `rate limit exceeded`. `code` is `rate-limit-exceeded`.

`POST /v1/tutor` and `POST /mcp` with method `tools/call` count toward 10 requests per 60 seconds for one anonymous client, or 120 for one `ipa_live_` key. The client without a key is `CF-Connecting-IP`, or the first `X-Forwarded-For` hop when that header is missing. The Workers Rate Limiting bindings are `TUTOR` (`namespace_id` `ipa-api-anonymous`, limit 10, period 60) and `TUTOR_KEY` (`namespace_id` `ipa-api-key`, limit 120, period 60). An in-memory window is the fallback for local tests and the native process. GET pages, `/health`, `/openapi.json`, `initialize`, and `tools/list` are not counted. A key is `Authorization: Bearer` or `X-API-Key`. An unrecognized key is 401. The store keeps a SHA-256 hash, a label, a short prefix, and audio versus IPA counters. No route lists secrets.

A POST body over 400 KiB is status 413, `code` `body-too-large`, and is refused from `Content-Length` before the body is read. `PUT /v1/uploads/{id}` uses the 8 MiB upload cap the same way.

`create_upload` keeps 32 slots. When they are all still live the response is status 503, `code` `upload-slots-full`, and `Retry-After` is the seconds until the oldest slot expires. The call does not drop another caller's wav.

The headers follow [draft-ietf-httpapi-ratelimit-headers](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers), which is still a draft:

```http
RateLimit-Policy: "tutor";q=10;w=60
RateLimit: "tutor";r=9;t=60
```

A key sends `RateLimit-Policy: "tutor";q=120;w=60` instead.

`q` is the quota, `w` is the window in seconds, `r` is how many requests remain, and `t` is whole seconds until the oldest counted request leaves the window. A 429 also sets `Retry-After` to `t`.

## Bad gateway

Status 502. The recognizer returned an error. `detail` starts with `recognizer returned`.

## Service unavailable

Status 503. Audio was sent and neither `RECOGNIZER_URL` nor the `ZIPA` service binding is available. An IPA request does not need either. `detail` on the worker is `RECOGNIZER_URL is unset and the ZIPA service binding is missing. IPA requests do not need either.` The native process says `RECOGNIZER_URL is empty` when that variable is blank.

A warming ZIPA container is also 503. `detail` starts with `recognizer returned 503` and the response sets `Retry-After`. Retry that request. It is not a 502.

`create_upload` slot exhaustion is 503 with `code` `upload-slots-full`.

## Audio URL

`audio_url` is https, or an upload from `create_upload`. The worker rejects `http`, userinfo, and hosts that are localhost, `*.local`, `*.internal`, or an IP literal in a private, loopback, link-local, multicast, or unspecified range, including IPv4-mapped forms of those addresses. It follows at most 3 redirects and checks every hop. Connect budget is 5 seconds when the transport reports it. The total budget is 20 seconds. The body is read in chunks and aborted at 256 KiB, the same cap as decoded `audio_base64`. `Content-Type` must be `audio/wav`, `audio/x-wav`, `audio/wave`, or `audio/vnd.wave`.

The `code` values are `audio-url-not-https`, `audio-url-host`, `audio-url-userinfo`, `audio-url-redirect-host`, `audio-url-redirects`, `audio-url-status`, `audio-url-type`, `audio-url-too-large`, `audio-url-connect`, and `audio-url-timeout`. The body is `application/problem+json`.

## Unscored audio

Audio shorter than 0.12 s after the trim is HTTP 200. `proximity` is null and `unscored` is `too_short`. That is not a problem document.
