Errors
How the Signal API reports errors: the problem format, every error type, refusals and rate-limit headers, and how to handle them.
When a request fails, the Signal API answers with an HTTP error status and a JSON body in the standard problem format (RFC 9457). The content type is application/problem+json. Every error has the same shape, so your code can handle them all in one place.
The shape of an error#
This is the answer to a request without a valid sign-in or API key:
{
"type": "https://api.odysseysignal.app/problems/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Authentication failed.",
"instance": "/v1/organizations"
}| Field | What it holds |
|---|---|
| type | A URL that names the kind of error. Use it, not the title or detail, to decide what your code does. It identifies the error; it is not a page to open. |
| title | A short summary of the kind of error. The same for every error of that type. |
| status | The HTTP status code, the same as on the response. |
| detail | What went wrong with this request, in one or two sentences you can show to a person. |
| instance | The path of the request that failed. |
Some errors add fields of their own, such as errors for an invalid request or reason for a refusal. Ignore fields you do not know: new ones can be added without a new API version.
Error types#
Every type starts with https://api.odysseysignal.app/problems/. The table lists the end of the URL.
| Status | Type | What it means |
|---|---|---|
| 400 | invalid-request | The request is well formed, but it asks for something that cannot be answered, for example a metric or dimension the DataSet does not have. The detail names what was asked for and what is available. |
| 400 | query_too_large | The query would read more data than one query may read, so it was not run. Ask a narrower question: a shorter period, or fewer markets, metrics or breakdowns. |
| 401 | unauthorized | The sign-in token or API key is missing, expired or not valid. The answer is always the same, whatever the cause. |
| 403 | forbidden | Your credential is valid, but it does not give access to what you asked for, or access is blocked for your organization or account. |
| 404 | not-found | The resource does not exist, or your credential does not give access to it. The two cases answer the same. |
| 404 | no-such-route | No endpoint has this path. Check the path against the API reference. |
| 422 | validation-failed | The request does not match the contract: a field is missing, has the wrong type or an invalid value. See the errors field. |
| 429 | rate_limited | Too many requests in a short time. Wait as long as Retry-After says, then try again. |
| 500 | internal-error | Something went wrong on our side. If it keeps happening, tell us the path and the time of the request. |
| 503 | unavailable | A service the request depends on did not answer, so the request was not run. Try again after Retry-After. |
| 405 and others | http-error | Any other HTTP error, such as a method the endpoint does not support. The detail says which. |
Invalid requests (422)#
A validation-failed error lists every invalid field in errors. Each entry has the location of the value, a message and a machine-readable type:
{
"type": "https://api.odysseysignal.app/problems/validation-failed",
"title": "Request does not match the contract",
"status": 422,
"detail": "One or more fields are invalid. See `errors` for each.",
"instance": "/v1/...",
"errors": [
{
"location": ["body", "metrics", 0],
"message": "Field required",
"type": "missing"
}
]
}location is the path to the value: where it is (body, query or path), then the field names and list positions inside it.
Refusals (403 and 429)#
When a request is refused even though your credential is valid, the error adds two fields for your code:
reason: which kind of refusal it is, such asrate_limitororganization_blocked.opens_at: when the request can succeed again, as an ISO 8601 time in UTC, ornullwhen it does not open again by itself.
The detail says what happened, when it opens again and who can change it, usually your organization's administrator.
Rate limits#
Requests that count against a rate limit carry these headers on every answer, not only when you are refused, so you can slow down before you reach the limit:
| Header | What it holds |
|---|---|
| X-RateLimit-Limit | How many requests the limit allows. |
| X-RateLimit-Remaining | How many you have left. |
| X-RateLimit-Reset | Seconds until the limit resets. |
| X-RateLimit-Scope | Which limit the numbers describe, such as queries. |
| Retry-After | On a 429 or 503 only: seconds to wait before you try again. |
A request counts against the limit even when it fails, for example with a 400 or 422.
Which requests count against which limit, and how to handle limits in your code: see Rate limiting.
Handling errors#
- Decide by
statusandtype. Showdetailto a person; do not parse it, because its wording can change. - Retry only a 429 or 503, and only after the time in
Retry-After. Retrying any other error gives the same answer. - Fix the request on a 400 or 422: the error names the field or value that is wrong.
- On a 401, get a new sign-in token or check the API key. The answer does not say which part was wrong. See Authentication.
- On a 500, note the
instanceand the time and contact us if it keeps happening.
Last reviewed 9 November 2026