Error envelope
Every 4xx and 5xx response from every module (Speech, Interview, CV
DeepMatch, CV DeepSearch) and from the cross-module credits/health endpoints
uses the
same envelope. Learn it once.
The shape
{
"error": {
"code": "INVALID_INPUT",
"message": "A human-readable explanation of what went wrong.",
"timestamp": "2026-05-29T10:00:00.000Z",
"requestId": "req_1705412345678_abc123",
"details": { /* optional, code-specific extras */ }
}
}
| Field | Always present? | Meaning |
|---|---|---|
error.code | Yes | A stable, machine-readable code. Branch on this, not on message. |
error.message | Yes | Human-readable description. May change wording over time — don't parse it. |
error.timestamp | Yes | ISO 8601 time the error was generated. |
error.requestId | When available | Per-response trace handle for support. Present once a route handler has run; middleware-level rejections (e.g. a bad API key) happen before one is generated and omit it. |
error.details | When applicable | Code-specific structured extras (e.g. field-level validation errors). |
requestId ≠ id ≠ externalIdrequestId identifies this one HTTP response, for tracing. It is not
the run's id and not your externalId
correlation tag — see Identifiers.
Rate limiting and warm-up
Responses that ask you to retry — 429 rate limits and 503
service-warming — additionally carry error.retryAfterSeconds and the
standard HTTP Retry-After header. Respect the header.
Branching on errors
resp = call_zenhire(...)
if not resp.ok:
err = resp.json()["error"]
if err["code"] == "RATE_LIMIT_EXCEEDED":
backoff_and_retry()
elif err["code"] == "INSUFFICIENT_CREDITS":
alert_billing()
else:
log(err["code"], err.get("requestId"))
Module error catalogs
The envelope is universal; the set of codes is module-specific. See each module's error reference, plus the shared catalog:
- Speech error codes — submit/poll/run codes.
- Interview, CV DeepMatch, and CV DeepSearch codes are listed on each endpoint page in the API reference (every status code shows its full response schema).