Calling the API
A client sends its token with every request and gets JSON back. This page describes requests, answers and errors, and the two optional protections of writes: idempotency keys and signatures.
GET /api/v1/projects?filter[status]=running&limit=20 HTTP/1.1Host: app.example.comAuthorization: Bearer anv_x7Kq2mP9aB3c_...-
Tokens are sent in the
Authorizationheader only, never in the URL. -
Every answer carries
X-Request-Id, also found in the access log. -
Answers are JSON (
{"data": ...}); lists add"next". -
Errors are problem documents (RFC 9457,
application/problem+json):{"type": "urn:anvil:problem:validation", "title": "...", "status": 422, "requestId": "...", "errors": {"name": "The Name field is required."}}
| Status | Meaning |
|---|---|
| 400 | Malformed request, invalid cursor or idempotency key |
| 401 | Missing, unknown, revoked or expired token; inactive or expired client (WWW-Authenticate: Bearer) |
| 403 | Missing permission, IP address not allowed, invalid signature, policy |
| 404 | Unknown path, or a record the client may not see |
| 405 | Known path, other method |
| 409 | Idempotency key used for another request |
| 415 | Body is not application/json |
| 422 | Invalid input, errors per field |
| 429 | Rate limit, Retry-After |
Validation messages are always English ($apiLocale), independent of the
locale of the application.
Idempotent writes
Section titled “Idempotent writes”Endpoints that allow it (create, change and actions of resources; custom
endpoints with idempotent()) accept an Idempotency-Key header (8–128
characters of A-Za-z0-9_.:-). The answer is stored for 24 hours and
repeated for the same key, with Idempotent-Replayed: true. The same key
with another body is refused with 409.
Signed requests
Section titled “Signed requests”If the client requires signatures, every request carries
X-Anvil-Signature: t=<unix time>, v1=<hex HMAC-SHA256>over t + "." + METHOD + "." + target + "." + body with the signing secret.
The target is the path with /api/v1/ and the query string exactly as sent;
the body is empty for requests without one:
1761400000.POST./api/v1/projects.{"name":"Alpha"}1761400000.GET./api/v1/projects?filter%5Bstatus%5D=running&limit=50.Requests older or newer than $apiSignatureWindow (300) seconds are
refused, and every signature is accepted once: identical requests in the
same second (same method, target and body) need a second’s pause.
$timestamp = time();$target = $path . ($query === '' ? '' : '?' . $query);$signature = hash_hmac('sha256', $timestamp . '.' . $method . '.' . $target . '.' . $body, $signingSecret);$header = 't=' . $timestamp . ', v1=' . $signature;See Client examples for complete clients.