Errors
The envelope, and what to branch on.
Every error is the same shape.
{
"error": {
"type": "invalid_request_error",
"code": "validation_failed",
"message": "duration_minutes must be between 5 and 120",
"param": "duration_minutes",
"request_id": "req_8f2c...",
"doc_url": "https://docs.experthire.io/..."
}
}
Branch on code, never on message. Messages are written for people and change;
codes are a closed set.
Quote request_id when you contact us. It is on every response, including successes.
The ones you will actually hit
| Code | Status | What to do |
|---|---|---|
invalid_api_key | 401 | Check the prefix. An ehp_ key is the wrong product |
secret_key_required | 401 | You sent a session token to a server-only endpoint |
validation_failed | 400 | Read param |
insufficient_credits | 402 | Stop and tell the customer |
sandbox_quota_exceeded | 402 | The month's sandbox allowance is used up |
assessment_not_found | 404 | Wrong id, wrong organisation, or wrong environment |
report_not_ready | 404 | Not completed yet. Sandbox returns a sample instead |
rate_limited | 429 | Back off for Retry-After |
idempotency_key_reuse | 409 | Same key, different body |
Everything else
| Code | Status | What to do |
|---|---|---|
missing_api_key | 401 | No Authorization header |
session_expired | 401 | The browser session lapsed or was revoked. Mint a new one |
launch_token_expired | 401 | The link outlived its window. Mint a new launch link |
launch_token_used | 401 | Single use. Mint a new one; do not retry the same link |
api_not_enabled | 403 | The Public API is off for this organisation. Contact us |
module_disabled | 403 | That assessment type is not enabled. Nothing was charged |
live_key_required | 403 | The endpoint reads or writes production data with no sandbox equivalent |
origin_not_allowed | 403 | Allowlist it with POST /v1/domains first |
key_not_found | 404 | Wrong key id, or it belongs to the other environment |
resume_ingest_in_flight | 409 | A resume is already processing. Poll, do not re-upload |
judging_in_progress | 409 | Submissions are still being scored. Retry finalize |
invalid_request | 400 | The body could not be parsed at all |
invalid_cursor | 400 | Cursors are opaque. Pass back what we gave you, unmodified |
invalid_origin | 400 | Send scheme and host only, e.g. https://app.example.com |
invalid_environment | 400 | Unknown environment name |
assessment_started | 400 | Too late to change it. Cancel instead |
candidate_not_editable | 400 | Shared with another organisation, or that email is taken |
key_revokes_self | 400 | Send a grace window, or rotate first and revoke after |
language_not_allowed | 400 | Read GET /v1/coding-languages and the test's allowed_languages |
unsupported_operation | 400 | That call does not apply to this assessment type |
unprocessable_resume | n/a | Not an HTTP status. It arrives as error_code on GET /v1/assessments/{id}/resume |
Three of these are worth reading twice.
live_key_required is not a bug to work around. Branding, members and analytics are
organisation-level with no sandbox equivalent, so a test key is refused rather than shown
production data.
resume_ingest_in_flight means the upload succeeded and scoring is running. Re-uploading
starts nothing; poll GET /v1/assessments/{id}/resume until it reads scored or failed.
judging_in_progress is the retry signal on finalize, not a failure. The assessment is
still resumable until it returns 200, so do not mark it complete on the 409.
Not-found means several things
An id from another organisation, or from the other environment, returns
assessment_not_found rather than a permission error. That is deliberate: a distinct
error would let an id be probed for existence across tenants.
Retrying safely
Send Idempotency-Key on every create. A retry then replays the original response
instead of creating a second assessment and billing twice.
A failed request releases its key, so a retry after a 500 does the work rather than replaying the failure.
Next
- Reference: rate limits, pagination, idempotency