Errors
Errors return a consistent JSON envelope and a matching HTTP status code. Branch on the status code and the structured error field, not on the human-readable message.
Error envelope
Every error response body has this shape:
{
"error": "validation",
"message": "Request failed validation.",
"issues": [
{ "path": ["reason"], "message": "Required" }
],
"requestId": "req_01HXY..."
}| Field | Always present | Notes |
|---|---|---|
error | yes | Stable machine-readable identifier. Branch on this. |
message | yes | Human-readable summary. Do not parse for logic. |
issues | only for validation errors | Field-level details, with path (JSON pointer-like array) and message. |
requestId | yes | Unique request ID. Include in support tickets. |
The same requestId is also returned in the X-Request-Id response header on every response (success or error).
Status codes
| Status | error identifier | Meaning |
|---|---|---|
400 | validation | The request body or query failed validation. Inspect issues to see which fields failed. |
401 | unauthenticated | No bearer token, or the token is invalid or expired. Re-issue a token. |
403 | forbidden | Authenticated but the token’s scopes don’t permit this action, or the workspace forbids it. |
404 | not_found | The resource does not exist or is not visible to this token. |
409 | conflict | The action conflicts with the current state (e.g., creating a record with a controlKey that already exists). |
410 | gone | The endpoint has been sunset (see Versioning). |
422 | unprocessable | The request shape was correct but the action is semantically invalid in the current context. |
429 | rate_limited | You exceeded the rate limit. Honor the Retry-After header. See Rate limits. |
5xx | internal | Server error. Retry safely after a short delay. Include the requestId if you escalate. |
Idempotency keys
Most write endpoints accept an Idempotency-Key header. Use one whenever your integration can’t guarantee at-most-once delivery (which is most real-world scenarios: network retries, queue redeliveries, scheduled job overlaps).
POST /api/v1/governance/findings
Idempotency-Key: scanner-batch-2026-05-21-item-3947
Content-Type: application/json
{ ... }Semantics:
- First call: processed normally. The system stores the response keyed by
(token, idempotency-key). - Repeated call with the same key: returns the original response (same status code, same body, same headers including
requestId). The action is not re-executed. - Repeated call with the same key but a different body: rejected with
409 conflictanderror: "idempotency_mismatch". This prevents accidentally repurposing a key. - Retention: stored responses are retained for 24 hours. After that, the key is forgotten and a repeated request will be processed as new.
Idempotency keys must be unique strings between 8 and 128 characters. Use a value that uniquely identifies the intent of the call (a row ID from your source system, a scan ID, etc.), not a random UUID generated per attempt.
Idempotency keys are scoped to the service-account token that made the original call. The same key from a different token does not match.
Retry guidance
When and how to retry:
| Status | Retry? | How |
|---|---|---|
2xx | n/a | succeeded. |
400, 403, 404, 409, 422 | No. | These represent client-side problems that won’t fix themselves. Fix the request, then call again. |
401 | Yes, once after refreshing the token. | If the new call also returns 401, treat as authentication misconfiguration. |
410 | No. | The endpoint is gone. Migrate to the replacement (see Link header). |
429 | Yes, after honoring Retry-After. | Use exponential backoff if you hit it repeatedly. |
500, 502, 503, 504 | Yes, with exponential backoff. | Pair with an idempotency key on writes to avoid double-processing. Cap the retries (5 is a sensible default). |
Common retry pattern:
attempt = 1
while attempt <= 5:
call_api(idempotency_key=...)
if success or non-retryable: return
sleep(min(2^attempt * 100ms + jitter, 30s))
attempt += 1Validation error details
Validation errors (400 / error: "validation") include a structured issues array:
{
"error": "validation",
"message": "Request failed validation.",
"issues": [
{ "path": ["reason"], "message": "String must contain at least 10 character(s)" },
{ "path": ["subjectResourceId"], "message": "Expected string, received null" }
],
"requestId": "req_01HXY..."
}path is an array describing how to navigate into the request body to find the offending field. For top-level fields, it’s a single-element array. For nested fields, it’s the full traversal: ["pages", 2, "title"] means “the title of element index 2 of the pages array.”
Common error scenarios
”I’m getting 409 conflict on the same controlKey”
You’re trying to create a control with a key that already exists in your workspace. Either pick a different key, or fetch the existing record by listing with a filter and update it instead.
”I’m getting 401 after a few hours of successful calls”
Your access token expired. Re-exchange the client credentials for a fresh token. Don’t request a new token per call; cache the token until shortly before its expires_in.
”I’m getting 403 on every call from one service account”
Either:
- The token’s scopes don’t include the action you’re attempting. Check the response: it tells you which scope was needed.
- The service account was disabled by a workspace admin.
”I’m getting 429 repeatedly even after waiting”
Your integration’s call volume exceeds the rate limit for your token. See Rate limits; contact your account team if you need a higher limit for a documented use case.
Including request IDs in support tickets
When something looks wrong, always include the requestId (from the response body or the X-Request-Id header) in your support ticket. With the request ID, Novantra support can find the exact request in internal logs and tell you what happened.