Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

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. Every version 1 operation is currently beta. While an operation is beta, Steer Phones may still correct its shape — a renamed field, a tightened type — without a new major version, and does so with notice rather than silently: an email to every integration holding a key, and a release note in this documentation. An operation is promoted to stable once the first integrations have run end-to-end against it; from that point, any change that would break a conforming client requires a new major version in the URL, exactly as above. Depend on stable operations for anything you cannot easily change; treat beta as subject to revision with notice, and deprecated as scheduled for removal.

Beta revisions

Because every version 1 operation is beta, the section above promises that a corrected shape is published rather than changed underneath you. This section is that record. Entries stay here after the change ships, so an integration built earlier can find out what moved.

Transcription status now reports current availability

transcriptionStatus on the call-log detail and voicemail detail operations described what the last transcription attempt did. It now describes whether transcript text is retrievable right now.

  • Before. A call or voicemail whose transcript had been removed — by your retention settings or by an explicit deletion — kept reading completed, with its transcript text empty or absent. The status recorded that transcription had once succeeded.
  • After. The same record reads unavailable and returns no text. completed now means, and only means, that transcript text is stored and returned by this read. The other three values are unchanged: processing while transcription is queued or running, failed when an attempt did not succeed, and unavailable when there is no text to return.

No field was added, renamed, or removed, and no record whose transcript is still present changes status. If your integration read completed as proof that a transcript had existed at some point, read it as a statement about the current response instead — and use unavailable on a record you previously stored as the signal that the transcript has since been removed. The bulk reporting rows introduced with this release use the same four values with the same meaning, so one mapping covers all three operations.

Not every list operation pages the same way

Pagination previously stated, without exception, that every list operation shares the page-number envelope. The call-log reporting collection introduced with this release pages by cursor instead. That statement is now scoped to the standard list operations, and the exception is named where the promise is made. Nothing about the existing list operations changed. See Pagination.

Deleting a tag that retained calls carry

Tag management is a dashboard action rather than an API operation, but it governs data these operations return, so the change is recorded here. A tag that any call in your retained history still carries can no longer be deleted, because that call’s tags are part of its record. The dashboard points you at Inactive instead, which retires the tag without rewriting the calls that carry it and keeps its name reserved in its group.

While this release was rolling out there was a short window in which deleting such a tag failed with a generic error rather than that guidance. If a tag deletion failed unexpectedly during that window, retry it: you will now get either the deletion or the Inactive guidance. See Organize calls with tags and tag groups.

Pagination

The standard list operations page the same way, so one pager works for all of them. One operation family is a deliberate exception, described at the end of this section.

Two request parameters control the page:

  • page — 1-indexed, defaulting to 1.
  • pageSize — defaulting to 25, with a maximum of 100. A larger value is a validation error, not a silent clamp.

List requests are strict: a query parameter the operation does not document is a validation error. (Responses are the opposite — additive — per the section above.)

Every list response uses one envelope:

{
  "data": [],
  "total": 1204,
  "page": 3,
  "pageSize": 25,
  "totalPages": 49
}

total and totalPages count the rows matching your filters — for a filtered list they describe the filtered set, not the whole resource. data holds one page of those rows.

Ordering is deterministic: rows come back newest first, by createdAt descending, with the row identifier as the tiebreak. Two rows created in the same instant therefore always order the same way, and the same request over the same data always yields the same pages.

The data underneath can still move. These are live lists ordered newest-first, so a row created while you are walking the pages pushes every older row down one position, and the next page you request can repeat a row you already received. Two rules make that harmless:

  • Deduplicate by identifier whenever you page a window that is still receiving rows.
  • Backfill inside a closed date window. For a historical import, do not walk from page 1 of the unbounded list: set dateTo to the moment you start, walk that window to the end, then pick up anything newer with a later pass — or let webhooks deliver it. A window whose upper bound is in the past no longer gains rows, so its pages do not shift.

The reporting collection pages by cursor

GET …/call-log-reports is the documented exception to everything above. It is a bulk extraction over a time window you bound yourself, and rows can move inside that window while you walk it — which is the case a page number handles worst, because a row arriving underneath you shifts every later page. So it pages by cursor:

  • Ascending by the timestamp you selected, then by identifier. Oldest first, not newest first.
  • {data, nextCursor, hasMore} — no total, no page, no pageSize, and no page count.
  • limit instead of pageSize, and a required half-open [dateFrom, dateTo) window.

Send nextCursor back unchanged until hasMore is false. The full rules — window selection, cursor validity and restart, duplicate handling, optional transcripts, and the limits these operations enforce — are in Use the customer API operations.

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 required scope, or the credential type is wrong for the route — a dashboard sign-in session, or a key that is not a customer integration key.
NOT_FOUND 404 The record — or the phone system in the path — does not exist or is not covered by the key’s grant. The two cases are deliberately indistinguishable.
CONFLICT 409 The request contradicts current state. Re-read, then decide.
RECORDING_NOT_READY 409 The audio exists but is still processing. Retryable — wait briefly, then request the URL again.
RATE_LIMITED 429 Back off and retry after retry_after.
EXPORT_TOO_LARGE 413 The response is too large to deliver, and nothing was truncated. Narrow the request where the operation allows it; follow that operation’s own guidance.
EXTERNAL_SERVICE_TIMEOUT 502 or 503 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 — RECORDING_NOT_READY is one, returned by the signed-URL operation while audio is still processing. 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, dependency timeouts, and a still-processing artifact (RECORDING_NOT_READY) — are marked retryable. A validation failure, a permission failure, or any other 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 over a 60-second window. A key is issued on one of two tiers:

Tier Allowance
standard 120 requests per 60 seconds
premium 600 requests per 60 seconds

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 limit applies to the key’s traffic as a whole, not to any single operation — which is why the operation reference does not repeat a 429 entry on each operation. Every operation can return one.

The budget belongs to the key, not to a phone system. A key whose grant covers several phone systems — a group or organization key — spends that one budget across all of them, so an integration iterating N systems is dividing a single allowance N ways. Ask for the premium tier as the default on any multi-system key; standard suits a single-system integration. The tier is chosen when the key is issued, and Steer Phones 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.

Two operations carry an extra budget of their own on top of the key’s tier: the call-log reporting collection and its dimensions snapshot are each limited to 120 requests per minute per key. These requests also consume the key’s overall allowance: a standard key has 120 requests per minute across all operations combined. They are also the only operations that refuse rather than continue when the rate limiter itself cannot be consulted — the same 429, for a different reason. Their throughput and recovery are described under Limits and the failures they produce.

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.