# Errors

_Error response shape, common codes, and where to look in the audit log._

_Last updated: 2026-05-07_

---

# Errors

The API uses standard HTTP status codes plus a machine-readable `code` field in the body so callers can branch programmatically.

## Response shape

Every error response carries this shape:

```json
[component]
```

The `error` field is for logs and operator eyes. The `code` field is what your error-handling code branches on. Some codes carry extra fields specific to the error (e.g. `INSUFFICIENT_BALANCE` carries `priceMicros` and `topUpUrl`).

## Common codes by category

### Auth

- `UNAUTHORIZED` (401) — no auth header present, or the header didn't resolve to a customer.
- `TOKEN_INVALID` (401) — the API key or wallet JWT signature didn't verify.
- `INVALID_SIGNATURE` (401) — for x402, the recovered EIP-3009 signing address didn't match the claimed `from`.
- `INVALID_NONCE` / `CHALLENGE_EXPIRED` / `NONCE_ALREADY_USED` (400/401) — wallet-JWT challenge/verify path; the nonce flow expects a single-use, TTL-bounded nonce.
- `INVALID_WALLET_ADDRESS` (400) — wallet address parameter wasn't well-formed.
- `MISSING_FIELDS` (400) — the verify body was missing required fields.

### Billing

- `INSUFFICIENT_BALANCE` (402) — the wallet doesn't have enough funds for the call. Body carries:

  ```json
  [component]
  ```

  `retryable: false` means re-issuing the same call won't succeed; the customer needs to top up first. For x402 callers, the top-up is an on-chain transfer; for API-key callers, the top-up is the Stripe portal link in `topUpUrl`.

### Validation

- `MISSING_PDF` (400) — multipart upload had no `pdf` part.
- `MISSING_PAYLOAD` (400) — multipart upload had no `payload` part.
- `INVALID_JSON` (400) — `payload` was not valid JSON.
- `VALIDATION_ERROR` (400) — payload parsed, but a field failed validation. Body carries the offending field path under `details`.
- `OUT_OF_SCOPE_V1` (400) — request exceeded the per-envelope cap (>10 signers OR >10 documents).

### Envelope state

- `ENVELOPE_NOT_FOUND` (404) — no envelope with that id is owned by the calling customer. The wrapper scopes lookups to the authenticated customer; an envelope that exists for someone else returns 404, not 403.
- `CANNOT_DELETE_COMPLETED` (400) — DELETE on a `COMPLETED` envelope is rejected; completed envelopes are terminal.
- `ENVELOPE_ALREADY_COMPLETE` (400) — resend on a `COMPLETED` envelope.
- `ENVELOPE_CANCELLED` (400) — resend on a `CANCELLED` envelope.
- `ALL_SIGNED` (400) — resend when every recipient has already signed (no NOT_SIGNED recipients to chase).
- `ENVELOPE_NOT_COMPLETE` (400) — download attempted on a non-`COMPLETED` envelope.
- `DOWNLOAD_FAILED` (502) — the underlying signing engine returned an unexpected response on the PDF stream.

### Templates

- `TEMPLATE_NOT_FOUND` (404).
- `INVALID_TEMPLATE_ID` (400) — template id wasn't a UUID.
- `RECIPIENT_NOT_FOUND` (404) — `from-template` recipient mapping referenced a recipient role that doesn't exist on the template.
- `INVALID_TEMPLATE_RECIPIENT_ID` (400).

### Fields

- `FIELD_CREATION_FAILED` (502) — fields-add call to the engine failed.
- `INVALID_FIELD_ID` (400) — field id wasn't a valid identifier.
- `DELETE_FAILED` (502) — field-delete call to the engine failed.

### Webhooks

- `INVALID_WEBHOOK_URL` (400) — URL wasn't well-formed, or was on the deny-list (private IP ranges, the API's own origin, etc.).
- `WEBHOOK_NOT_FOUND` (404).

### Upstream

- `DOCUMENSO_UNAVAILABLE` (503) — the underlying signing engine is reachable but errored. Retry with exponential backoff against the same idempotency key.
- `RELAYSTATION_UNAVAILABLE` (503) — billing facilitator errored. Same retry guidance.
- `SEND_FAILED` (502) — `POST /v1/envelopes/id/send` failed at the engine layer.
- `UPDATE_FAILED` (502) — template update failed at the engine layer.

## Idempotency and retries

Billable operations require an `Idempotency-Key` header. A retry with the same key returns the original response (success OR error) without re-running the side effect.

For 5xx upstream errors, retry with the same key is the right move. For 4xx errors, the retry will return the same 4xx — fix the request, then retry with a fresh key.

## What you don't see

A few things that look like errors but aren't surfaced as error responses:

- **Webhook delivery failures** — when the wrapper's POST to your URL fails, your registration's `consecutiveFailures` counter increments, but no error appears on a customer-side API call. Check `GET /v1/webhooks/id/deliveries` to debug.
- **Refund failures** — if a refund-on-terminal flow fails partway through, the refund is logged as failed and the operator is alerted. The terminal state on the envelope (`REJECTED` etc.) doesn't change; the customer doesn't see an API error from the original `POST /v1/envelopes` (it succeeded, then later failed terminally).
- **Backfill / migration errors** — operator-side internal flows. Customers don't see them.

## Audit log

Every customer-side mutation that persists state writes to an audit log row owned by the customer. There's no customer-facing API for the audit log in V1; it's an operator surface. If you need to investigate why an envelope ended up in an unexpected state, the operator can pull the audit history and explain.

In V1.x the operator may expose a per-customer audit-read API; that's not a V1 commitment.
