Errors
Loom API v1 returns a consistent JSON error body on every failure. This page documents the response format, the HTTP status codes, and the error codes the API can return.
Error Response Format
All errors share one flat JSON structure:
{
"error": "INVALID_REQUEST_BODY",
"message": "Human-readable description of what went wrong",
"details": { "field": "additional context" },
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
Response fields
error— machine-readable error code (a string; see the tables below).message— human-readable description. Optional.details— additional context. Optional; present only for some errors.requestId— UUID identifying the request. Also returned in thex-request-idresponse header. Include it when contacting support.
There is no nested error.code object — the value of error is the code string.
HTTP Status Codes
| Status | Meaning |
|---|---|
400 Bad Request | Invalid request body or parameters |
401 Unauthorized | Authentication failed |
402 Payment Required | Subscription inactive |
403 Forbidden | Authenticated but not permitted |
404 Not Found | Route or resource not found |
409 Conflict | Conflicting state (e.g. already provisioned) |
429 Too Many Requests | Rate limit or quota exceeded |
500 Internal Server Error | Unexpected server error |
503 Service Unavailable | A dependency (cache) is unavailable |
Error Codes
Authentication
| Code | Status | Meaning |
|---|---|---|
UNAUTHENTICATED_TENANT | 401 | The x-tenant-api-key header is missing, invalid, or revoked. |
FORBIDDEN | 403 | The key is valid but not permitted for this resource. |
{
"error": "UNAUTHENTICATED_TENANT",
"message": "The provided API key is invalid or expired",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
Solutions: confirm you are sending the x-tenant-api-key header (not Authorization: Bearer), and that the key is active in the dashboard.
Validation
| Code | Status | Meaning |
|---|---|---|
INVALID_REQUEST_BODY | 400 | The request body is malformed JSON or contains unknown fields. |
INVALID_PARAMETERS | 400 | A query or path parameter is missing or invalid. |
ROUTE_NOT_FOUND | 404 | No route matches the request. |
POST /verify/start accepts only the optional userAgent and ip string fields. Any other field is rejected with INVALID_REQUEST_BODY.
Verification
| Code | Status | Meaning |
|---|---|---|
VERIFICATION_NOT_FOUND | 404 | No verification matches the given verificationId. |
VERIFICATION_ALREADY_COMPLETED | 400 | The verification has already reached a terminal status. |
VERIFICATION_MUST_USE_WEBHOOK | 400 | POST /verify/complete is unavailable for iDenfy-backed verifications; results arrive via the webhook callback. |
Rate limiting & quota
| Code | Status | Meaning |
|---|---|---|
RATE_LIMIT_EXCEEDED | 429 | Too many requests in the current window. |
QUOTA_EXCEEDED | 429 | The plan's monthly quota has been reached. |
HARD_LIMIT_EXCEEDED | 429 | An absolute usage ceiling has been reached. |
429 responses set a Retry-After header when a retry delay is known, and may include details.retryAfterSeconds. See Rate Limits.
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded",
"details": { "retryAfterSeconds": 30 },
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
Billing & tenant
| Code | Status | Meaning |
|---|---|---|
BILLING_INACTIVE | 402 | The subscription is inactive; resolve billing in the dashboard. |
TENANT_INACTIVE | 401 | The tenant account is disabled. |
NOT_PROVISIONED / TENANT_NOT_PROVISIONED | 409 / 403 | The tenant has not been provisioned. |
Server
| Code | Status | Meaning |
|---|---|---|
INTERNAL_ERROR | 500 | Unexpected server error. Retry; if it persists, contact support with the requestId. |
DATABASE_ERROR | 500 | A database operation failed. |
REDIS_ERROR | 503 | The cache service is unavailable. |
Token validation is not an error
POST /tokens/validate always returns HTTP 200. An invalid, expired, or revoked token is reported in the response body — not as an error envelope:
{ "valid": false, "over18": false, "reason": "TOKEN_EXPIRED" }
reason is one of OK, TOKEN_EXPIRED, TOKEN_REVOKED, TOKEN_NOT_FOUND, UNDERAGE_OR_UNKNOWN. See Verification.
Handling Errors
Retry logic
Retry 5xx and 429 responses; never retry other 4xx responses — they will not succeed on retry.
async function requestWithRetry(url, options, maxRetries = 3) {
let lastError
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options)
if (response.ok) return response
const body = await response.json().catch(() => ({}))
// 4xx other than 429 is a client error — do not retry
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
throw new Error(`${body.error}: ${body.message}`)
}
lastError = new Error(`${body.error}: ${body.message}`)
if (attempt < maxRetries) {
const retryAfter =
Number(response.headers.get('Retry-After')) || 2 ** attempt
await new Promise((r) => setTimeout(r, retryAfter * 1000))
}
}
throw lastError
}
Log the requestId
const response = await fetch('https://api.loomapi.com/verify/start', {
method: 'POST',
headers: {
'x-tenant-api-key': process.env.LOOM_TENANT_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
if (!response.ok) {
const body = await response.json()
console.error('Loom API error', {
code: body.error, // e.g. "INVALID_REQUEST_BODY"
message: body.message,
requestId: body.requestId, // also in the x-request-id response header
status: response.status,
})
}
Always quote the requestId when contacting support@loomapi.com — it lets us locate your request in the logs.
More
- Authentication — tenant API key setup
- Rate Limits — limits, headers, and backoff
- Verification — full endpoint reference