# Errors, pagination and idempotency

These conventions apply to every endpoint. Read this once and you will know how to handle any API response. The full [API Reference](/api-reference) lists every endpoint, its parameters, and its response shapes.

## Error shape

All errors return JSON with two fields:

```json
{
  "err_type": "<string>",
  "err_msg": "<string>"
}
```

`err_type` is a machine-readable enum. `err_msg` is a human-readable description of what went wrong.

**Error types:**

| HTTP status | `err_type` | Typical cause |
|---|---|---|
| `400` | `parse_error` | Malformed JSON or missing required fields |
| `401` | `unauthorized` | Auth headers missing, invalid signature, expired timestamp, or reused nonce |
| `403` | `forbidden` | Administrative Policy denied the action, Signing Policy denied signing, or key is frozen |
| `404` | `not_found` or `resource_not_found` | The requested resource ID does not exist |
| `409` | `resource_conflict` | Conflict: typically a duplicate caller-supplied ID |
| `422` | `invalid_inputs` | Syntactically valid JSON but semantically invalid values |
| `500` | `internal_server_error` | Transient server-side error; safe to retry with backoff |

The error `Content-Type` is always `application/json`, except for `415 Unsupported Media Type` responses which return plain text.

## Common auth errors

**`401 unauthorized` with a timestamp message:** your `x-request-timestamp` is more than 5 minutes from server time. Check your clock is synchronized (NTP) and that you encode the timestamp correctly as a u64 little-endian integer in milliseconds.

**`401 unauthorized` with a nonce message:** you reused a nonce. Generate 8 fresh random bytes for every request.

**`401 unauthorized` with a signature message:** the Ed25519 assertion does not verify. Common causes: wrong field order in the transcript, wrong encoding of the nonce (hex string vs. raw bytes in the transcript), including the `ApiKey ` scheme prefix in the transcript (strip it), or signing the transcript bytes directly rather than the SHA-256 digest.

**`403 forbidden` after successful auth:** the caller does not have the required Administrative Policy permission. Check the `err_msg` for the specific capability name.

## Pagination

All list endpoints use keyset (cursor-based) pagination. The response always includes:

```json
{
  "items": [ /* ... */ ],
  "total": 200,
  "next_cursor": "<opaque string or null>"
}
```

`total` is the total count of items matching the filter (not just this page). `next_cursor` is null when you have reached the last page.

To fetch the next page, pass the value of `next_cursor` as the `cursor` query parameter on the next request. Keep all other query parameters the same.

```bash
# First page.
curl "$CV_BASE/v1/keys?limit=20"

# Next page (paste the cursor value from the previous response).
curl "$CV_BASE/v1/keys?limit=20&cursor=<next_cursor_value>"
```

:::warning\[Cursor format varies by endpoint]
The audit log endpoint uses an integer cursor (`uint64`), while other list endpoints use an opaque string cursor. Treat all cursor values as opaque and do not manipulate them.
:::

Allowed `limit` values are 1 through 100 inclusive. The default varies by endpoint but is always within that range.

## Async operations and idempotency

Most mutation endpoints (create, update, delete) are asynchronous. They return either:

* `200 OK` with the completed resource.
* `202 Accepted` with `{"status": "processing", "request_id": "<string>"}`.

Poll the [request status endpoint](/api-reference#tag/requests), `GET /v1/requests/{request_id}`, until the status is no longer `"processing"`. Polling is free: it requires only the `Authorization` header and never causes side effects.

All create endpoints accept a **caller-supplied `id`** field. The vault uses this ID as the primary identifier. If you send the same request twice with the same `id`, the second request returns `409 resource_conflict`. This gives you a natural idempotency mechanism: pick a deterministic ID for each resource and the vault guarantees you only create it once.

The sign endpoint (`POST /v1/keys/{key_id}/sign`) is always synchronous and has no idempotency key. Each call is an independent signing operation.

## Retry guidance

| Condition | Retry? | Strategy |
|---|---|---|
| `500 internal_server_error` | Yes | Exponential backoff, cap at ~60 seconds |
| `401 unauthorized` (clock skew) | Yes | Fix the timestamp; retry immediately |
| `401 unauthorized` (nonce reuse) | Yes | Generate a new nonce; retry immediately |
| `401 unauthorized` (bad signature) | No | Fix the transcript construction |
| `403 forbidden` | No | Fix permissions or unfreeze before retrying |
| `404 resource_not_found` | No | Verify the ID |
| `409 resource_conflict` | No | The resource already exists; use it or choose a different ID |
| `422 invalid_inputs` | No | Fix the request body |
| Network timeout on a mutation | Conditional | If you received a `request_id` before the timeout, poll it to check if the operation succeeded before retrying |

When retrying a mutation that timed out without a `request_id`, check whether the resource was created (using a GET) before retrying the create to avoid a `409` conflict.
