Skip to content

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.1
Host: app.example.com
Authorization: Bearer anv_x7Kq2mP9aB3c_...
  • Tokens are sent in the Authorization header 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.

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.

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.