API versioning and errors
Read a Steer Phones problem response, branch on its error code, retry safely, and stay compatible as version 1 grows.
A client that survives platform changes does two things: it tolerates additions it did not expect, and it reacts to failures by machine-readable code rather than by reading English. This page covers both — how version 1 is allowed to change, and the exact failure envelope every operation returns.
What version 1 may change
The major version is part of the base URL, https://api.steerphones.com/v1.
Inside version 1, these changes can happen without warning and are not breaking:
- A new operation appears.
- A new field appears on an existing response.
- A new optional field is accepted on an existing request.
- A new error code appears for a condition that previously used a broader one.
So parse leniently: ignore response fields you do not recognize, do not validate responses against a closed schema, and do not treat an unknown error code as a parsing failure — fall back to the HTTP status. A change that would break a conforming client requires a new major version in the URL, published with migration guidance; version 1 is not retired underneath you.
Each published operation also carries a lifecycle stage in its approval record — stable, beta, or deprecated. Depend on stable operations for anything you cannot easily change; treat beta as subject to revision and deprecated as scheduled for removal.
The failure envelope
Every documented failure returns application/problem+json (RFC 9457) with a flat body:
| Field | Always present | Meaning |
|---|---|---|
code |
Yes | The stable machine identifier. This is what your code branches on. |
status |
Yes | Mirrors the HTTP status code. |
title |
Yes | Short human summary of the problem type. |
detail |
Yes | A user-safe explanation. Display it if you must; never match on it. |
type |
Yes | A URL identifying the problem type. An identifier to match, not a link. |
correlationId |
Yes | The request identifier to quote to support. |
retryable |
Yes | Whether this condition is transient. Derived from code, never from text. |
retry_after |
No | Seconds to wait before retrying, when a meaningful delay exists. |
errors |
No | Field-level validation problems. Present on validation failures. |
params |
No | Extra primitive detail about the condition, such as a limit that was hit. |
{
"type": "https://errors.steerphones.example/validation-error",
"title": "Validation error",
"status": 400,
"code": "VALIDATION_ERROR",
"detail": "The request contains invalid data.",
"correlationId": "9f3ab21c",
"retryable": false,
"errors": [{ "pointer": "/name", "code": "REQUIRED", "detail": "This field is required." }]
}
detail is user-safe by construction: internal messages, stack traces, and identifiers of other
tenants never reach it. That also means it is a poor branching key — it is written for a person.
Field-level validation errors
A validation failure returns 400 with code: "VALIDATION_ERROR" and an errors array. Each entry
names the offending field by JSON Pointer into the request body, along with a per-field code:
| Field code | Meaning |
|---|---|
REQUIRED |
The field was absent. |
TOO_SMALL |
Below the minimum allowed. |
TOO_BIG |
Above the maximum allowed. |
INVALID_FORMAT |
Present but wrongly formatted. |
INVALID |
Present but not an accepted value. |
Pointers let you highlight the exact input a person got wrong instead of showing a whole-request
failure — /eventTypes points at that member of the body you sent.
Codes worth handling
| Code | Typical status | What to do |
|---|---|---|
VALIDATION_ERROR |
400 | Fix the request. Use errors to say which field. |
AUTH_UNAUTHORIZED, AUTH_INVALID_TOKEN, AUTH_EXPIRED |
401 | Check the key is present, current, and not revoked. |
AUTH_FORBIDDEN |
403 | The key lacks the scope, or is for another phone system. |
NOT_FOUND |
404 | The record does not exist, or is outside this phone system. |
CONFLICT |
409 | The request contradicts current state. Re-read, then decide. |
RATE_LIMITED |
429 | Back off and retry after retry_after. |
EXTERNAL_SERVICE_TIMEOUT |
502 | A dependency timed out. Retry with backoff. |
EXTERNAL_SERVICE_ERROR |
502 or 503 | A dependency is unavailable. Do not hammer it; alert instead. |
INTERNAL_ERROR |
500 | Capture correlationId and contact support. |
Some conflicts carry a more specific code than CONFLICT so a client can react precisely rather
than parsing a message. Handle the specific codes you know, and fall back to the status for the rest.
Retry safely
retryable is computed from the error code, so it never drifts from the message text. Only
genuinely transient conditions — rate limiting and dependency timeouts — are marked retryable. A
validation failure, a permission failure, or a conflict will fail identically no matter how often you
resend it.
For writes, one more rule applies: do not retry a write unless the published reference for that operation states that retrying is safe. A timeout means the request may have succeeded, so blind retries can duplicate work. Where the reference documents no retry guarantee, re-read the state and decide from there.
Use exponential backoff with jitter, honour retry_after when present, and cap total attempts so a
sustained outage does not become an unbounded queue in your own system.
Rate limits
Requests are counted per API key. Every response carries the current window:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed in the current window. |
X-RateLimit-Remaining |
Requests left in the window. |
X-RateLimit-Reset |
Unix time, in seconds, when the window resets. |
Retry-After |
Seconds to wait. Sent with a 429 response. |
Over the limit, you get 429 with code: "RATE_LIMITED" and a retry_after value. The exact
allowance depends on the tier your key was issued with; Steer Phones tells you the rate when the key is
issued, and can raise it for an integration with a legitimate need. Design for the headers rather
than a hard-coded number — read X-RateLimit-Remaining and slow down before you are refused.
Spread bulk work over time instead of firing it in parallel bursts, and back off on 429 rather than retrying immediately.
When you contact support
Every response carries an X-Request-Id header, and every problem body repeats it as
correlationId. Quote that value, the approximate time, and the operation you called. It identifies
your exact request in Steer Phones’ records without exposing your key or the request body, so it is the
fastest route to an answer — and it is safe to paste into a ticket.