Error Handling Guide
The most important fact about Agility API errors: success codes are not automatic indicators of
success. The Bulk API answers 200 OK with failures itemised in the body,
and several classic traps return 200 with wrong data rather than any
error at all. Build your handling around inspecting responses, not around
status codes — and when something looks wrong despite a 200, start at the
troubleshooting hub.
| Status | What it means |
|---|---|
200 | The request was understood. On api/asset, commands may still have failed — always check commandFailures.count |
400 | A violation or bad value: the body carries a tag like Violation'AssetCannotDeleteClosed or a message like Wrong OID AssetType |
401 | Missing/invalid credentials — or access tokens disabled instance-wide; see authentication |
403 | Authenticated, but the member's role lacks the right |
404 | No such route or asset |
405 | Wrong HTTP method — notoriously, api/customFields is a POST |
500 | On api/asset, usually your unparseable body, returned opaquely with only a reference GUID — see errors and partial failures |
Turn Violation Tags into Readable Messages
Many 400s carry a tag, not prose. Some tags resolve through the
localization API
into human-readable, caller-language text — and unmatched tags echo back
unchanged, so resolving every error costs at worst one wasted request,
never a wrong message:
GET ~/loc-2.v1?[Violation'PrimaryWorkitemCannotHaveInvalidStatus]
→ { "…": "The chosen Status is not valid for the Backlog with the assigned Team" }
Handle Bulk API Partial Failure
api/asset is not transactional: successes apply, failures itemise. Each
failure's sourceCommandIndex maps back to your request, so the correct
retry unit is the failed commands only:
result = bulk(commands)
failures = result["commandFailures"]["commands"]
if failures:
retryable = [commands[f["error"]["sourceCommandIndex"]] for f in failures]
# fix or re-send just these — the successes are already applied
See errors and partial failures for the full
failure model, including why a missing body is a hard 500.
Retry Policy
- Retry: network errors, and
5xxresponses that are not the known unparseable-body500— with exponential backoff and jitter. - Do not retry:
400violations (the request will fail identically),401/403(fix credentials or rights), or Bulk API command failures without fixing the command. - Handle
429. An exceeded quota returns429with a JSON body identifying the limit and how long to wait; honorRetryAfterand back off rather than retrying immediately. See rate limiting. - Separately, a slow response is a signal to narrow your query, not to hammer with retries — the underlying constraint is shared database load.
Rate Limiting
Digital.ai-hosted instances enforce rate limits;
on-premises instances do so only once an administrator enables them. Quota
policies cap API usage by request time, size of data returned, number of
requests, and number of errors, each over a set duration. A request that
exceeds a limit gets a 429 response identifying the limit and how long to
wait before retrying. See rate limiting for the
full quota-policy and usage-metrics endpoints.
Timeouts
The server's SQL timeout applies per query, not per request — a bulk request carrying several large queries can legitimately run far longer than any single query. Size client timeouts for the whole request, and treat a query that brushes the timeout as a design problem: narrow the projection, tighten the filter, shrink the page.
What to Log
- The failing command payload and its violation tag (the tag is stable; localized text is not)
- For opaque
500s: the reference GUID — on a hosted instance it is the only handle support has; on-premise it keys into the host's exception log - The page/offset in long extractions, so a retry can resume rather than restart