Errors
One envelope, a stable code, and a request id on every response.
Every error is the same shape — type, code, message, doc_url, request_id — and nothing else ever leaves this API.
{
"error": {
"type": "invalid_request",
"code": "deadline_in_past",
"message": "A warrant deadline must be in the future.",
"doc_url": "https://docs.gowarrant.xyz/api-reference/errors#deadline_in_past",
"request_id": "req_537c915a-c1dd-4625-93a2-21fa7ec5d938"
}
}Branch on code, not on message. Codes are stable; messages are written for
people and may be reworded. type maps one-to-one onto the status, so it never
tells you anything the status line did not.
An unexpected server-side throw is normalised too. You get
type: "api_error", code: "internal_error" and a request id — never a stack
trace and never a driver message.
Request ids
Every response carries X-Request-Id, error or not. Send your own and it is
echoed rather than replaced, so a request id can be threaded from your logs
into ours.
POST /v1/warrants HTTP/1.1
Host: api.gowarrant.xyz
Authorization: Bearer wr_test_8f0c1b4e2b1a4d599d0e3a2f7c1b9e44
Idempotency-Key: 0d8f1a1e-6f2c-4c1a-9a3f-2b6e5d4c3b2a
Content-Type: application/json
{
"amount": 8400000,
"payee": "api.exampledata.io",
"deadline": "2026-08-16T12:00:00.000Z",
"spec": { "clauses": [{ "kind": "http.status", "equals": 200 }] }
}400
{
"error": {
"type": "invalid_request",
"code": "validation_failed",
"message": "amount: Invalid input: expected string, received number",
"doc_url": "https://docs.gowarrant.xyz/api-reference/errors#validation_failed",
"request_id": "req_537c915a-c1dd-4625-93a2-21fa7ec5d938"
}
}curl -sS https://api.gowarrant.xyz/v1/warrants \
-H "Authorization: Bearer $WARRANT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 8400000,
"payee": "api.exampledata.io",
"deadline": "2026-08-16T12:00:00.000Z",
"spec": { "clauses": [{ "kind": "http.status", "equals": 200 }] }
}'That example is the mistake most first integrations make: 8400000 as a JSON
number rather than "8400000" as a string. See
amounts.
Statuses
| Status | type | Meaning |
|---|---|---|
| 400 | invalid_request | The request is malformed or fails validation. |
| 401 | unauthenticated | No credentials, or credentials resolving to nobody. |
| 402 | insufficient_funds | Not enough available balance to hold. |
| 403 | forbidden | Authenticated, and refused. |
| 404 | not_found | No such thing, or it is another organisation's. |
| 409 | invalid_state / conflict | The transition is not legal, or an idempotency conflict. |
| 429 | rate_limited | Over the window. See rate limits. |
| 500 | api_error | Something broke on our side. |
Codes you are most likely to hit
Validation and shape
code | Status | Cause |
|---|---|---|
validation_failed | 400 | A field failed its schema. The message names the path. |
deadline_in_past | 400 | The deadline is not in the future. |
idempotency_key_required | 400 | A money-moving POST arrived without the header. |
Authentication and permission
code | Status | Cause |
|---|---|---|
not_authenticated | 401 | No credentials, or a key or cookie resolving to nobody. |
insufficient_role | 403 | Your role is below what the route needs. |
user_session_required | 403 | An API key tried something that needs a person. |
kill_switch_active | 403 | The organisation has stopped new warrants. |
passkey_unavailable | 403 | Above the threshold, and no verifier is configured. |
Money and state
code | Status | Cause |
|---|---|---|
insufficient_funds | 402 | Available balance will not cover the hold. |
warrant_not_found | 404 | No such warrant, or not yours. |
illegal_transition | 409 | e.g. releasing an already-refunded warrant. |
version_conflict | 409 | Someone else moved it first. Re-read and retry. |
idempotency_key_reused | 409 | Same key, different body. |
Sessions and policies
code | Status | Cause |
|---|---|---|
session_ceiling_exceeded | 403 | The spend session is out of headroom. |
session_warrant_limit_reached | 403 | The session has opened its allowance. |
session_failure_budget_spent | 403 | Consecutive failures exhausted the budget. |
session_expired / session_revoked | 403 | The spend session is over. |
policy_no_bands | 400 | A policy needs at least one band. |
policy_no_floor_band | 400 | A policy needs a band at minScore: 0. |
policy_band_bps_required | 400 | A partial_release band needs releasedBps in 0–10000. |
policy_band_out_of_range | 400 | minScore outside 0–1. |
Internal surfaces
These sit outside /v1 and authenticate with a named header.
code | Status | Cause |
|---|---|---|
invalid_worker_secret | 401 | X-Warrant-Worker absent or wrong. |
worker_secret_unset | 403 | No secret configured; the route is closed. |
telegram_webhook_unauthenticated | 401 | Webhook secret absent or wrong. |
telegram_webhook_unset | 403 | No secret configured; the webhook is closed. |
Amounts are integer strings
Every amount on the wire is a string of digits in the asset's smallest unit,
validated against ^[0-9]+$.
"8400000" 8.40 USDC
8400000 rejected — a JSON number
"8.40" rejected — not the smallest unit9007199254740993 as a JSON number silently loses precision as a double, and
this is a payments API. The decimal point is a display concern.