Skip to main content
digital accessibility compliance for state and local government
Scan a site

Errors

Last updated 28 October 2026 · API version v1

Every failure this API reports is one document in one shape, so one branch in your client handles all of them and the interesting part is which type came back.

One shape, every failure

Errors are application/problem+json, per RFC 9457, which obsoletes RFC 7807 and keeps its media type. A 400, a 401, a 403, a 404 and a 429 are all the same five fields in the same order, and each carries the three rate-limit headers.

Nothing in the body ever carries a stack trace, a query fragment, or a hint that a resource you cannot see exists.

A complete document

This is a real response, not an illustration. A test key held by an organisation reads the sandbox’s City of Riverbend and nothing else, so it is asked here for the asset of Riverbend Public Library, which exists and is not its own:

bash: read another organisation’s asset

curl https://api.barrierops.com/api/v1/properties/15fc1ba9-d687-5f96-b06d-367f01abd3e9 \
  -H "Authorization: Bearer $BARRIEROPS_API_KEY"

Lines are wrapped for printing. The original is one line.

json: the document a request for a resource you cannot reach receives

{
  "type": "https://barrierops.app/problems/not-found",
  "title": "Not found",
  "status": 404,
  "detail": "No such resource, or it is not yours",
  "instance": "00000000-0000-4000-8000-000000000000"
}

Lines are wrapped for printing. The original is one line.

instance is the request id, which is different on every request; this page prints one fixed value in its place.

The fields

type
A stable URI naming the kind of failure. Match on this string rather than on the title — the title is prose and may be reworded, the type may not.
title
A short statement of what went wrong, in words.
status
The HTTP status, repeated in the body so a logged document is whole.
detail
What was wrong with this particular request, in a sentence, when there is something useful to say. Optional, and absent on a 500.
instance
The request id, different on every request. It is the reference to quote to support, and it is the same id the API logged the request under.

What can go wrong

The API reports 13 kinds, and a type is https://barrierops.app/problems/ followed by the name in the first column. The URI is an identifier rather than an address — it is stable and it is what you match on, and it is deliberately not a page that can move underneath a client. Some rows also describe the application’s own routes, such as the demo sign-in: the kinds are one module’s, shared by both.

Every kind of error the API reports, its HTTP status, the title it carries and what causes it. 13 rows.
TypeStatusTitleWhat causes it, and what to do
validation-failed400The request body or query is not validA parameter or a field of the body is missing, the wrong type, or outside its range — a limit above 200 is one — and detail names the field and what was wrong with it. A body that is not JSON is this kind too, and so, with status 415, is a body sent without Content-Type: application/json. Fix the request; retrying it unchanged fails the same way.
unauthenticated401Authentication is requiredNo key was sent, or the one sent was not accepted: unknown, revoked, expired, or a secret a rotation has retired. Every reason gets the same sentence, so a guessed key learns nothing, and the response carries WWW-Authenticate: Bearer realm="BarrierOps API". The demo sign-in, POST /auth/demo, sends this kind for a pair that is not the published one, titled “The demo credentials were not accepted”.
forbidden403Your role does not permit this actionThe credential is valid and may not do this. A key is refused with the title “This key does not permit this action” and a detail saying why: a test key asked to change something, or a key whose scopes do not include the one the operation needs. That refusal comes before the id is read, so it never tells you whether an id exists — see below. One 403 of this kind is not about a role or a scope: the demo sign-in, POST /auth/demo, sent from any site but the demo’s, titled “The demo sign-in is not available on this site”.
export-blocked403Exports are blocked in the demo organisationThe organisation is a demo organisation. Its data is fictional, so no file leaves it — no CSV, PDF or stored document. No role lifts it and retrying does not help; the same request in a real organisation is unaffected.
demo-read-only403This change is blocked in the demo organisationThe organisation is a demo organisation and its data is fictional, so this change is refused — detail says which. No role lifts it and retrying does not help; the same request in a real organisation is unaffected.
demo-disabled403This is not available in the demo organisationThe organisation is a demo organisation, and this control is turned off in it — detail says which, and why. No role lifts it and retrying does not help; the same request in a real organisation is unaffected.
not-in-edition403Your edition does not include this featureThe organisation’s edition does not include this feature — detail names the editions that do and the one the organisation is on. No role lifts it and retrying does not help; the same request from an organisation on one of those editions is unaffected.
read-only-session403Not available in a read-only staff sessionThe session is a BarrierOps staff member viewing the organisation in a read-only session, which appears in the organisation’s audit log with the person’s name and reason. Every write is refused, and the attempt is recorded. No role lifts it and retrying does not help.
agency-read-only403Not available through read-only agency accessThe caller reaches the organisation through an agency, and that access is read-only: the agency has suspended its management of the client, or the caller is an agency owner who is not assigned to it. An agency’s key reads a suspended client as before and is refused every change to it. Nothing is changed. The agency resuming the client lifts it; retrying does not.
not-found404Not foundNo such resource, or it is not yours. A genuine miss and another organisation’s resource get the same 404, with the same title and the same detail; only instance, the request id, differs. A deleted asset is still read by its id, with deletedAt set, and so are its issues and the pages each one is on; the lists under /properties/{id} answer this 404 for it.
conflict409The request conflicts with the current stateThe request is well formed and valid, and the current state will not accept it. detail says which state. Starting a scan is refused this way while the organisation already has as many unfinished scans as a key may start at once, and detail says how many.
rate-limited429Too many requestsThe budget for this minute is spent: the key’s own, or for a request without an accepted key, its network’s. Retry-After carries the wait in seconds, and the three rate-limit headers are on the response as they are on every other. The demo sign-in, POST /auth/demo, has a limit of its own, five sign-ins from one address in ten minutes: its 429 is titled “Too many sign-ins” and carries Retry-After alone.
internal500The request could not be completedSomething failed on our side. The 500 carries no detail — not even a constant one — because that is where a stack trace or a query fragment would reach a customer. Quote instance to support and it resolves to the log line. A 503 carries this kind as well, each with a detail and Retry-After in seconds, and each is worth retrying after that wait: “The sandbox is not ready” answers a test key while the sandbox organisations are still being prepared; “Sign-in is temporarily unavailable” while the identity provider cannot be reached; and two from the demo sign-in, POST /auth/demo — “No copy of the demo is ready” when none is free to claim, and “The demo is full” while every copy is in use. Billing adds two with a detail and no Retry-After: a 503, “Payments are not available”, when the deployment has no payment provider, and a 502 when the payment provider could not complete the request. Both say nothing was charged.

title is prose and may be reworded; type may not. A client that branches on the title is a client that breaks when an error message is improved — and a key’s 403 already has a title of its own beside the kind’s.

Why another organisation’s resource is a 404

Cross-tenant access returns 404 so an id cannot be probed: a 403 in its place would confirm that the id exists.

A genuine miss and a resource belonging to somebody else get the same response, from the same function: the same type, title and detail, with only instance, the request id, different — the example above is the second. That is deliberate: if the two differed at all — a different detail string would be enough — the difference would tell anyone with a list of ids which of them are real. A deleted asset is not a miss. It is still read by its id, with deletedAt set, and so are its issues and the pages each one is on; the lists under /properties/{id} answer the 404 for it.

A 403 is for a credential that may not do this: a role it lacks, a scope a key lacks, a test key asked to change something. A key refused for its scopes, or for being a test key, is refused before the id is read, so that 403 says nothing about whether the id exists. Once a key may make the change, an id outside what it reaches is the same 404 as a miss.

Quoting an error to support

Every error carries an instance value you can quote to support. It is the same reference the API logs.

Send the instance value and the time. It resolves to the log line for that request, which is the difference between a support conversation that starts at the failure and one that starts by asking you to reproduce it.

A 500 carries no detail at all — not even a constant one — because that field is where a stack trace or a connection string would reach a customer. The instance value is how we find out what it was. Report one through the contact page.

https://barrierops.com/docs/errors