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

# Errors, limits and request ids

> The two error shapes, every status and whether to retry it, how the API answers methods, formats and limits, and the id every response carries.

Every error the API answers has one of two shapes, decided by where the request went:

* **Under the FHIR base** (`/v1/fhir/r4`), a FHIR `OperationOutcome`, sent as
  `application/fhir+json`. A FHIR client reads it like any other resource.
* **Everywhere else**, an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem, sent as
  `application/problem+json`.

Each service's page lists the statuses that service answers for its own reasons. This page covers
the ones every service shares.

## The two shapes

<CodeGroup>
  ```json Problem (outside the FHIR base) theme={null}
  {
    "type": "https://docs.haau3.com/errors#invalid-request",
    "title": "Invalid request",
    "status": 400,
    "detail": "The query parameters are not valid. Each entry in issues names one.",
    "instance": "urn:uuid:0b6f2d4e-8c1a-4e3b-9f5d-2a7c6e1b4d90",
    "issues": [
      { "path": ["status"], "message": "Invalid option: expected one of \"free\"|\"busy\"|\"busy-unavailable\"|\"busy-tentative\"" }
    ]
  }
  ```

  ```json OperationOutcome (FHIR base) theme={null}
  {
    "resourceType": "OperationOutcome",
    "issue": [
      {
        "severity": "error",
        "code": "not-found",
        "details": {
          "text": "No resource, operation, or interaction is served at this path. GET [base]/metadata lists what this base serves."
        }
      }
    ]
  }
  ```
</CodeGroup>

A problem has these members:

| Member | What it holds |
| - | - |
| `type` | A link to this page's section for the problem. Branch on this, not on the text. |
| `title` | A short summary. It is the same for every problem of one type. |
| `status` | The HTTP status code, the same as the response's. |
| `detail` | What went wrong this time, and how to correct it. It is written for people, so do not parse it. |
| `instance` | This request's id, written `urn:uuid:<id>`: the same id as the response's `X-Request-Id` header. See [request ids](#request-ids). |
| `issues` | On a `400` only: one entry per parameter that is not valid, each with a `path` naming it and a `message` saying why. |

Problems may gain members over time. Ignore any member you do not recognise.

An `OperationOutcome` says the same things in FHIR's terms: each entry in `issue` carries a `code`
from FHIR's issue types and a `details.text` saying what went wrong.

## Statuses every service shares

| Status | Problem `type` | `OperationOutcome` code | Send it again? |
| - | - | - | - |
| `400` | [`invalid-request`](#invalid-request) | varies by service | No. Correct the request first. |
| `401` | [`unauthorized`](#unauthorized) | `login` | No. Send a valid key. |
| `404` | [`not-found`](#not-found) | varies by service | No. |
| `405` | [`method-not-allowed`](#method-not-allowed) | `not-supported` | No. Use a method the `Allow` header lists. |
| `406` | [`not-acceptable`](#not-acceptable) | `not-supported` | No. Accept JSON. |
| `413` | none: only the FHIR base reads a body | `too-long` | No. Send less: see [limits](#limits). |
| `415` | [`unsupported-media-type`](#unsupported-media-type) | `not-supported` | No. Send the body as JSON. |
| `429` | [`rate-limited`](#rate-limited) | `throttled` | Yes, after the `Retry-After` time, which for a monthly limit can be weeks. |
| `500` | [`server-error`](#server-error) | `exception` | Once, after a short wait. |
| `503` | [`unavailable`](#unavailable) | varies by service | Yes, after the `Retry-After` time when one is sent. |

A `502`, a `504`, and some `503`s come from a source behind a service: see
[when a source fails](#when-a-source-fails).

### Invalid request

`400`. Something in the request is not valid: a parameter, its value, or the body. Outside the FHIR
base, `issues` names each parameter at fault. On the FHIR base, the `OperationOutcome` names it. A
parameter the path does not take, or one sent twice, is a `400` too: see [parameters](#parameters).

### Unauthorized

`401`. No API key was sent, or the key sent was not recognised. The `detail` says which, and the
response carries `WWW-Authenticate: Bearer realm="api.haau3.com"`, with `error="invalid_token"`
added when a key was sent. See [Authentication](/authentication).

### Not found

`404`. Nothing is served at this path, or the record asked for does not exist. A path that does not
exist anywhere is a `404` in the base's shape: `GET /openapi.json` lists every path the API serves,
and `GET /v1/fhir/r4/metadata` lists what the FHIR base serves.

### Method not allowed

`405`. The path exists, but does not take the method sent. The `Allow` header lists the methods it
does take, for example `Allow: GET, HEAD, OPTIONS`.

### Not acceptable

`406`. The request asked for a format the API does not answer in. Every answer is JSON: see
[formats](#formats). On the FHIR base it is also the answer to a FHIR release this endpoint does not
serve.

### Unsupported media type

`415`. A body was posted without saying it is JSON. Send `Content-Type: application/fhir+json`
(`application/json` is accepted too).

### Rate limited

`429`. Your organization reached one of its plan's limits, which take effect on 2026-11-01. Two
can answer, each with a `Retry-After` header in seconds and a sentence naming the limit:

* **Requests a minute**, on every plan. The wait runs to the start of the next minute, so it is at
  most 60 seconds. Send the request again after it.
* **Requests a month**, on the Developer plan only. The wait runs to 00:00 UTC on the 1st of the
  next month, which can be weeks away, so don't send it again automatically. Change plan to keep
  going now.

Each plan's numbers are in [limits](#limits).

### Server error

`500`. The server failed to process the request. Send it again once, after a short wait. If it
fails again, the request is one we have to fix.

### Unavailable

`503`. The service cannot answer right now. When the response carries `Retry-After`, wait that
many seconds before sending the request again. On the FHIR base, the `OperationOutcome` says what is
unavailable: a source rate limiting requests is coded `throttled` (see
[when a source fails](#when-a-source-fails)), and a source this deployment does not have is coded
`not-supported`.

Any request can get a `503` with `Retry-After` when its API key could not be checked just now, coded
`transient` on the FHIR base. So can a Scheduling request when the scheduling database is
unavailable or slow. Neither says anything about the request: send it again after the wait.

### When a source fails

Four services answer from a source behind them: terminology (LOINC), the provider directory (CMS's
National Provider Directory), Questionnaires (LOINC, and a form's own publisher) and Plan Benefits'
`$in-network` (the provider directory). When the source fails, the service says so in an
`OperationOutcome`. It never answers with a guess, an empty result or "not found" in place of the
answer it could not get.

| Status | What happened | Send it again? |
| - | - | - |
| `502` | The source refused or failed the request, or answered in a form the service could not read. | Once, after a short wait. |
| `503` | The source is rate limiting requests. The code is `throttled`, and `Retry-After` says how many seconds to wait: 2, or longer when the source asked for longer. | Yes, after the `Retry-After` time. |
| `504` | The source did not answer in time. | Once, after a short wait. A search that is slow because of what it asks will be slow again, so narrow it first. |

Before answering any of these, the service has already asked its source a second time when that
could help, after waiting a moment, and waited as long as the source asked when that was 5 seconds
or less.

Inside a batch Bundle, a throttled entry answers `503` but carries no `Retry-After`, because an
entry has no headers of its own. Wait 2 seconds and send that entry again.

## Methods

* **`405` and `Allow`.** A method a path does not take answers `405` with an `Allow` header naming
  the methods it does take.
* **`HEAD`** answers exactly like `GET` on every path that takes `GET`, without the body.
* **`OPTIONS`** answers `204` with the `Allow` header and no body. Like every request under `/v1`,
  it needs an API key.

Inside a batch Bundle, an entry gets the answer the same request would get on its own. A `405`
there has no header to carry, so its `OperationOutcome` names the methods instead.

## Formats

Every answer is JSON. A request whose `Accept` header admits no JSON gets a `406` rather than JSON
it did not ask for. These admit JSON: `application/json`, `application/fhir+json`,
`application/json+fhir`, `text/json`, `application/*` and `*/*`, with any parameters. No `Accept`
header at all is fine too.

On the FHIR base, `_format` takes precedence over `Accept`: `json`, `application/json`,
`application/fhir+json`, `application/json+fhir` and `text/json` are served, and any other value
is a `406`.

A POST to the FHIR base must say its body is JSON, with `Content-Type: application/fhir+json` or
one of the types above. A body sent without one gets a `415`.

## Limits

Every API on the platform today is included in every plan. Free to start on the Developer plan.

From 2026-11-01, each plan has two limits, counted for your organization across all of its keys:

| Plan | Requests a month | Requests a minute | Past the month's allowance |
| - | - | - | - |
| Developer | 1,000 | 60 | Stops: a `429` until the 1st of the next month |
| Team | 50,000 | 600 | Keeps working, with no extra charge |
| Scale | 500,000 | 3,000 | Keeps working, with no extra charge |

A month is a calendar month in UTC. A request counts once, a batch included. A 5xx, a 401 and a 429
do not count. If an organization keeps going past its allowance, we'll get in touch about a plan
that fits. The minute limit counts every request sent with your organization's keys, and a request
past it is a `429` on every plan (see [rate limited](#rate-limited)).

Until 2026-11-01 no request is refused for a limit. Your organization's count for this month is on
the dashboard's Overview and Usage pages at [platform.haau3.com](https://platform.haau3.com), and
the plans are on [pricing](https://platform.haau3.com/pricing).

Every request also has bounds of its own, whatever the plan:

| What | Limit | Past it |
| - | - | - |
| Entries in a batch `Bundle` | 100 | `400`, coded `too-long`. Send the entries in smaller batches. |
| A request body on the FHIR base: a batch, or an operation's `Parameters` | 1 MiB | `413`, coded `too-long`. |
| The body of `$reconcile-medications` | 16 MiB | `413`, coded `too-long`. |
| Entries in the `Bundle`, or parameters in the `Parameters`, of `$reconcile-medications` | 50,000 | `400`, coded `too-long`. Send one patient's records per request. |
| `property` on `CodeSystem/$lookup`, the query and the body together | 100 | `400`, coded `too-long`. |
| How long a batch runs | 60 seconds | Each entry still running answers `504` inside the batch's `200`. Send it again on its own or in a smaller batch. |
| How long `$reconcile-medications` runs | 20 seconds | `504`. |

The largest batch most callers need is far smaller: a batch of 50 lookups is a few kilobytes.

A source or store that does not answer in time is a `504`. When your API key could not be checked,
or the scheduling database is unavailable or slow, the answer is a `503` with `Retry-After` (see
[unavailable](#unavailable)). Neither says anything about your request, so send it again after a
short wait.

A first call after a quiet period can take a few seconds. Calls after it are faster.

### Paging

A list that can be long comes back a page at a time, in one of four ways, each its service's own:

| Where | Page size | Next page | Also |
| - | - | - | - |
| Scheduling `/slots` and `/provider-locations` | `limit`, 1 to 100, 25 by default | `offset` | `total` counts every match. |
| Provider directory searches | `_count`, 1 to 100 | Follow the `next` link as given | Its `page` cursor is opaque and expires: an expired one is a `400`. See [paging](/provider-directory/provider-directory#paging). |
| `InsurancePlan` searches | `_count`, 1 to 50, 10 by default | Follow the `next` link as given | `total` on every page. A `410` means the plans changed: start the search again. See [paging](/plan-benefits/plan-benefits#paging). |
| `ValueSet/$expand` on a value set this service hosts | `count` | `offset` | An expansion from its own source comes back whole. |

Every other list comes back whole in one answer: Scheduling's `/publishers`, `/service-types` and
`/specialties`, a `Questionnaire` search (with `total`), `$counties` and `$in-network`.

## Caching and compression

Every JSON answer is compressed with gzip when the request's `Accept-Encoding` allows it, and says
so in `Content-Encoding`. Most HTTP clients ask for this and undo it without being told. The
[Slot Publisher](/scheduling/slot-publisher)'s NDJSON files are sent uncompressed.

| What | `Cache-Control` | Also |
| - | - | - |
| `GET /openapi.json` | `public, max-age=300` | An `ETag`. Send it back in `If-None-Match` for a `304` when the spec has not changed. |
| `GET /v1/fhir/r4/metadata` | `private, max-age=300` | An `ETag`, as above. An answer that says a store could not be read just now is `no-store`. |
| `$reconcile-medications` | `no-store` | On every answer, errors included: it carries patient data. |

## Parameters

Each operation takes the parameters the [API reference](/api-reference) lists for it, and no
others. A parameter it does not take gets a `400` that names it, rather than an answer as if it had
not been sent, so a misspelt filter never comes back as an unfiltered answer that looks right.
Every parameter at fault is named at once: outside the FHIR base, one entry in `issues` each, its
`path` naming the parameter; on the FHIR base, one `OperationOutcome` issue each, its `expression`
naming the parameter. The message lists the parameters the operation does take.

```json theme={null}
{
  "type": "https://docs.haau3.com/errors#invalid-request",
  "title": "Invalid request",
  "status": 400,
  "detail": "The query parameters are not valid. Each entry in issues names one.",
  "instance": "urn:uuid:0b6f2d4e-8c1a-4e3b-9f5d-2a7c6e1b4d90",
  "issues": [
    { "path": ["startafter"], "message": "Not supported. Supported parameters: status, publisher, serviceTypeCode, npi, specialty, appointmentType, acceptingPatients, isVirtual, city, state, startAfter, startBefore, includePast, providerLocationKey, limit, offset." }
  ]
}
```

* **A parameter sent twice** is refused the same way, unless the API reference says it repeats, as
  `property` does on `CodeSystem/$lookup`. Scheduling's lists (`serviceTypeCode`, `npi` and
  `specialty`) are one parameter whose values are separated by commas.
* **A read** (`GET [base]/[type]/[id]`), **a batch** and **`$reconcile-medications`** take no
  parameters. A batch entry carries its own in its `request.url`, and each entry is checked as the
  same request on its own would be.
* **`_format`** belongs to the FHIR base and is taken on every path there (see
  [formats](#formats)). Outside the FHIR base it is not a parameter, and is refused like any other.
* **A `Parameters` body**, when an operation is called with POST, follows the same rules: a
  parameter the operation does not take is refused, each parameter needs a name and a value, and a
  name sent both in the URL and in the body counts as sent twice.

Names are checked before values. Once every parameter is one the operation takes, a value that is
not valid is named in the same way.

## Request ids

Every response carries an `X-Request-Id` header holding a UUID that haau3 gave the request, errors
included. A problem carries the same id as its `instance`, written `urn:uuid:<id>`. On the FHIR
base the id is in the header only, and the `OperationOutcome` is unchanged.

To ask about a response, email [hello@haau3.com](mailto:hello@haau3.com) with its `X-Request-Id`
and the time you sent the request. haau3 keeps its record of a request for 30 days, so ask within
that time.

You can also send an id of your own. Put a UUID in the request's `X-Request-Id` header, and the
response carries it back in `X-Correlation-Id`, beside haau3's id. haau3 keeps your id with its
record of the request, so a question can name either one. A value that is not a UUID is ignored:
it is not kept, and it does not come back.

```http theme={null}
GET /v1/fhir/r4/metadata HTTP/1.1
Authorization: Bearer <key>
X-Request-Id: 3b8e7a52-9c41-4f0e-b6d2-7a1c5e9f0d84

HTTP/1.1 200 OK
X-Request-Id: 0b6f2d4e-8c1a-4e3b-9f5d-2a7c6e1b4d90
X-Correlation-Id: 3b8e7a52-9c41-4f0e-b6d2-7a1c5e9f0d84
```

## Changes

Changes a caller can see are recorded on the [changelog](/changelog) from 2026-11-01.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.