---
id: error-handling
title: 'Error Handling Guide'
description: 'How Agility API errors actually present, and how to handle them: partial failures, violation tags, retries and timeouts'
sidebar_label: 'Error Handling'
sidebar_position: 2
tags: ['guides', 'errors']
---

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](/troubleshooting).

| 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](/getting-started/authentication#common-authentication-errors) |
| `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](/apis/field-visibility/custom-fields)              |
| `500`  | On `api/asset`, usually **your unparseable body**, returned opaquely with only a reference GUID — see [errors and partial failures](/apis/asset-bulk/errors) |

## Turn Violation Tags into Readable Messages

Many `400`s carry a tag, not prose. Some tags resolve through the
[localization API](/apis/conventions/errors#violation-tags-are-localizable)
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:

```text
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**:

```python
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](/apis/asset-bulk/errors) for the full
failure model, including why a missing body is a hard `500`.

## Retry Policy

- **Retry**: network errors, and `5xx` responses that are not the
  known unparseable-body `500` — with exponential backoff and jitter.
- **Do not retry**: `400` violations (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 returns `429` with a JSON body
  identifying the limit and how long to wait; honor `RetryAfter` and back
  off rather than retrying immediately. See [rate limiting](#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](/guides/query-size-and-performance#extracting-at-scale-full-loads-and-delta-sync).

## Rate Limiting {#rate-limiting}

Digital.ai-hosted instances enforce [rate limits](/guides/rate-limiting#hosted-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](/guides/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](/guides/query-size-and-performance).

## What to Log

- The failing command payload and its violation tag (the tag is stable;
  localized text is not)
- For opaque `500`s: 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

## Related Topics

- [Troubleshooting by symptom](/troubleshooting)
- [Bulk API errors and partial failures](/apis/asset-bulk/errors)
- [Error conventions](/apis/conventions/errors)
- [Authentication errors](/getting-started/authentication#common-authentication-errors)
- [Rate limiting](/guides/rate-limiting)
