Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Errors and limits

Error shape

Errors Granite raises are JSON with one field:

{"error": "approved scopes cannot broaden the requested scopes"}
  • 401 is always {"error":"unauthorized"} and 403 is always {"error":"forbidden"}. Neither says why, by design.
  • 5xx is always {"error":"internal error"}; details are kept out of responses.
  • Other 4xx messages are written for people and may change. There are no error codes, except that a few messages start with a fixed word you can match on: owner_identity_retired:, no_warden_key_enrolled:, no_warden_ceremony:, other_account:, already_connected:.
curl -sS https://granitegranted.com/v1/approval-requests | grep -Fx '{"error":"unauthorized"}'

Some errors are not JSON. They come from request parsing, before Granite looks at your credential, and are plain text:

StatusCauseExample body
400A path id is not a UUID, a query value is not allowed, or the body is not valid JSONFailed to deserialize query string: status: unknown variant `bogus`, ...
413Body larger than 64 KiB
415Body sent without Content-Type: application/jsonExpected request with Content-Type: application/json
422Body is JSON but a required field is missing or has the wrong typeFailed to deserialize the JSON body into the target type: missing field `title` ...
curl -sS -o /dev/null -w '%{http_code}\n' 'https://granitegranted.com/v1/approval-requests?status=bogus' | grep -Fx 400

Status codes

StatusMeaning
200, 201, 204Done. POST /v1/approval-requests and POST /v1/connections return 201; DELETE /v1/me returns 204.
400A field breaks a rule: empty or too-long text, a non-https URL, an expires_at in the past, approved scopes wider than requested, a grant request with no requester_chirp_sub where a Warden passkey is needed.
401No credential, or one Granite does not accept. Also a missing or invalid x-user-id on an app credential or machine token.
403The credential is valid but may not do this: an app trying to decide, list or export, verifying for another actor without the enforcer privilege, a grant from another environment, or a Warden approval without a valid assertion.
404Not found, or not yours. Granite does not say which.
409The request is no longer pending, its deadline has passed, or the app’s link to the person points to a ChirpAuth identity that no longer signs in (owner_identity_retired:; the person must reconnect your app in Granite). For a person: the request or connection link was made for a different account (other_account:), or the connection is already connected to someone else (already_connected:).
429Too many requests filed; see below.
503A service Granite depends on (such as ChirpAuth) could not be reached. Retry later.

POST /v1/grants/verify reports “not approved” as 200 with "approved": false, not as an error. See API examples.

Rate limits

  • Filing. POST /v1/approval-requests and POST /v1/connections share one budget: bursts of up to 20, refilling at 10 per minute. An app or machine client has a separate budget for each user it files for, and for each agent it names in requester_chirp_sub. Everything it files for one user is also capped at bursts of 60, refilling at 30 per minute. Over either, you get 429 with {"error":"too many approval requests; retry later"}. It is a safeguard, not a quota to plan around.
  • Per IP address. More than 2,000 requests from one IP address in 5 minutes are blocked at the edge with 403, until the rate falls. This response is not Granite’s JSON.

Granite has no other quotas.

Size limits

LimitValue
Request body64 KiB
x-user-id128 characters
title, requested_action, trace_id160 characters
summary1000 characters
requested_resource, each entry of requested_resources500 characters
requested_scopes50 entries, 160 characters each
callback_url, detail_url, return_url500 characters
structured_detail16 KiB, as serialized JSON
A decision reason500 characters