Granite
Beta Base URL: https://granitegranted.com
Overview
Granite asks a person for permission on behalf of an app or agent, and keeps the answer.
- Your app files an approval request: what it wants to do, to what, and how risky it thinks that is.
- The person signs in to Granite at https://granitegranted.com with their ChirpAuth account, reads the request, and approves or denies it.
- Granite records the decision. If the request asked for standing authority, approving it creates a grant.
- Before acting, your app (or the service that carries out the action) asks Granite whether the grant still covers what it is about to do. The person can revoke a grant at any time.
Granite does not carry out the action and cannot stop you from acting without asking. It answers “did this person approve this, and does that approval still hold?” Checking that answer is your service’s job.
It is for developers of apps and agents that must get a person’s explicit approval before acting for them, and for services that enforce those approvals by checking a grant before each use.
What beta means here: Granite runs in production, and Stackwell’s own services use it. App credentials are issued by hand: there is no self-serve sign-up. Some features are not finished yet.
Quickstart
This walks one approval through Granite: your app asks, a person decides, and your app reads the answer.
1. Check that Granite is reachable
No credentials needed:
curl -fsS https://granitegranted.com/health
{"service":"granite","status":"ok"}
2. Get an app credential
Email support@granitegranted.com with:
- the app id you want (a short name such as
acme), and - the display name people should see on the approval screen.
Stackwell registers the app and sends you a secret that starts with gas_.
Granite stores only a hash of it and cannot show it again; if you lose it, ask
for a new one, which replaces the old.
There is no self-serve sign-up yet.
3. File an approval request
Name the person in x-user-id by the ChirpAuth subject id (sub) your app
received when they signed in to it with ChirpAuth. Granite does not treat it
as proof of who the person is: it says which ChirpAuth account may claim the
request (see Who owns a request).
curl https://granitegranted.com/v1/approval-requests \
-H "Authorization: Bearer $GRANITE_APP_SECRET" \
-H "x-granite-app-id: acme" \
-H "x-user-id: sub_8Q3K2M11XZ" \
-H "Content-Type: application/json" \
-d '{
"request_type": "one_time_action",
"title": "Send the weekly report",
"summary": "Acme wants to email this week'\''s report to your team list.",
"risk_level": "low",
"requested_action": "send email",
"requested_resource": "mailto:team@example.com"
}'
Expected response, 201 Created:
{
"id": "ee2bd1ff-f8f6-4eba-ae40-cdc7188a998a",
"owner_uid": null,
"owner_bound": false,
"requester_user_ref": "sub_8Q3K2M11XZ",
"requester_app_id": "acme",
"environment": "Prod",
"request_type": "one_time_action",
"title": "Send the weekly report",
"summary": "Acme wants to email this week's report to your team list.",
"risk_level": "low",
"requested_action": "send email",
"requested_resource": "mailto:team@example.com",
"requested_scopes": [],
"expires_at": null,
"status": "pending",
"created_at": "2026-11-05T19:19:06.527831736Z",
"updated_at": "2026-11-05T19:19:06.527831736Z"
}
owner_bound: false means no person owns the request yet. requester_app_id
comes from your credential, whatever the body says.
4. Send the person to the request
Give the person this link, built from the id above:
https://granitegranted.com/consent/ee2bd1ff-f8f6-4eba-ae40-cdc7188a998a
They sign in with ChirpAuth, see your request, and approve or deny it. Only
the ChirpAuth account you named in x-user-id can claim it; anyone else who
opens the link is told it was filed for a different account.
After that first request, Granite remembers which person your x-user-id
belongs to. Later requests with the same x-user-id are bound to them when
you file them (owner_bound: true) and appear in their Granite inbox without
a link.
5. Read the outcome
Poll the request with the same credential:
curl https://granitegranted.com/v1/approval-requests/ee2bd1ff-f8f6-4eba-ae40-cdc7188a998a \
-H "Authorization: Bearer $GRANITE_APP_SECRET" \
-H "x-granite-app-id: acme" \
-H "x-user-id: sub_8Q3K2M11XZ"
When the person has decided, status is approved or denied, and
decision_id and decided_at are set. To be told instead of polling, set
callback_url when you file the request; see
API examples.
6. Check before you act
For a request that creates a grant, check the grant every time you rely on it, because the person can revoke it at any time:
curl https://granitegranted.com/v1/grants/verify \
-H "Authorization: Bearer $GRANITE_APP_SECRET" \
-H "x-granite-app-id: acme" \
-H "x-user-id: sub_8Q3K2M11XZ" \
-H "Content-Type: application/json" \
-d '{"grant_id": "b6ddef35-fe77-43be-a871-bb80d528906a", "scopes": ["drive.read"]}'
Act only if the response has "approved": true. See
Concepts for the rules.
Authentication
An app calls Granite with an app credential (Authorization: Bearer gas_...
plus x-granite-app-id), issued by Stackwell on request; see
step 2. An agent or service can instead use a
ChirpAuth machine token from a ChirpAuth client Granite accepts. Either
way, you name the person by their ChirpAuth subject id in x-user-id.
People approve and deny with their own
Granite sign-in, which apps cannot use. Authentication has the
headers and a table of what each credential may do.
API reference
The full endpoint list is in the generated API reference. API examples walks through the endpoints an app uses, with real requests and responses, and Concepts explains the rules they enforce.
Conventions shared by every endpoint:
- Base URL
https://granitegranted.com; every path starts with/v1/. - Request and response bodies are JSON; send
Content-Type: application/json. - Times are RFC 3339 in UTC. Ids are UUIDs.
- Lists are not paged.
- There are no idempotency keys: every
POST /v1/approval-requestsfiles a new request.
Errors and limits
Errors are JSON, {"error": "<message>"}, except parsing errors, which are
plain text. Filing requests is rate-limited per caller, and request bodies are
capped at 64 KiB. Errors and limits lists the status
codes, the limits, and where each limit applies.
Data and privacy
Granite keeps requests, decisions and grants with no time limit, and it learns no email address or name. Deleting an account marks the person’s records deleted: Granite stops returning them, and their grants stop verifying. Data and privacy says what is stored, who sees it, and what deletion does.