Errors and limits
Error shape
Errors Granite raises are JSON with one field:
{"error": "approved scopes cannot broaden the requested scopes"}
401is always{"error":"unauthorized"}and403is always{"error":"forbidden"}. Neither says why, by design.5xxis always{"error":"internal error"}; details are kept out of responses.- Other
4xxmessages 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:
| Status | Cause | Example body |
|---|---|---|
400 | A path id is not a UUID, a query value is not allowed, or the body is not valid JSON | Failed to deserialize query string: status: unknown variant `bogus`, ... |
413 | Body larger than 64 KiB | |
415 | Body sent without Content-Type: application/json | Expected request with Content-Type: application/json |
422 | Body is JSON but a required field is missing or has the wrong type | Failed 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
| Status | Meaning |
|---|---|
200, 201, 204 | Done. POST /v1/approval-requests and POST /v1/connections return 201; DELETE /v1/me returns 204. |
400 | A 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. |
401 | No credential, or one Granite does not accept. Also a missing or invalid x-user-id on an app credential or machine token. |
403 | The 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. |
404 | Not found, or not yours. Granite does not say which. |
409 | The 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:). |
429 | Too many requests filed; see below. |
503 | A 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-requestsandPOST /v1/connectionsshare 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 inrequester_chirp_sub. Everything it files for one user is also capped at bursts of 60, refilling at 30 per minute. Over either, you get429with{"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
| Limit | Value |
|---|---|
| Request body | 64 KiB |
x-user-id | 128 characters |
title, requested_action, trace_id | 160 characters |
summary | 1000 characters |
requested_resource, each entry of requested_resources | 500 characters |
requested_scopes | 50 entries, 160 characters each |
callback_url, detail_url, return_url | 500 characters |
structured_detail | 16 KiB, as serialized JSON |
A decision reason | 500 characters |