- Under the FHIR base (
/v1/fhir/r4), a FHIROperationOutcome, sent asapplication/fhir+json. A FHIR client reads it like any other resource. - Everywhere else, an RFC 9457 problem, sent as
application/problem+json.
The two shapes
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
A
502, a 504, and some 503s come from a source behind a service: see
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.
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.
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. 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.
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), 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.
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
405andAllow. A method a path does not take answers405with anAllowheader naming the methods it does take.HEADanswers exactly likeGETon every path that takesGET, without the body.OPTIONSanswers204with theAllowheader and no body. Like every request under/v1, it needs an API key.
405
there has no header to carry, so its OperationOutcome names the methods instead.
Formats
Every answer is JSON. A request whoseAccept 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:
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).
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, and
the plans are on pricing.
Every request also has bounds of its own, whatever the plan:
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). 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:
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’sAccept-Encoding allows it, and says
so in Content-Encoding. Most HTTP clients ask for this and undo it without being told. The
Slot Publisher’s NDJSON files are sent uncompressed.
Parameters
Each operation takes the parameters the API reference lists for it, and no others. A parameter it does not take gets a400 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.
- A parameter sent twice is refused the same way, unless the API reference says it repeats, as
propertydoes onCodeSystem/$lookup. Scheduling’s lists (serviceTypeCode,npiandspecialty) are one parameter whose values are separated by commas. - A read (
GET [base]/[type]/[id]), a batch and$reconcile-medicationstake no parameters. A batch entry carries its own in itsrequest.url, and each entry is checked as the same request on its own would be. _formatbelongs to the FHIR base and is taken on every path there (see formats). Outside the FHIR base it is not a parameter, and is refused like any other.- A
Parametersbody, 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.
Request ids
Every response carries anX-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 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.