File approval requests, learn the outcome
Your app or agent asks the user for permission through Granite. The user decides; you receive the outcome correlated by the requester_user_ref you sent. Granite never learns their email or name, so it never gives you either.
The model
Register an app
Receive an app credential
x-granite-app-id.File approval requests
The user decides in Granite
Learn the outcome
Verify standing grants before acting
File a request
Describe what you want, where it reaches, and the risk you assess. Granite renders it to the user and holds the pending decision until they act or it expires. Name the user in x-user-id by the ChirpAuth subject id (sub) your app received when they signed in to it; only that ChirpAuth account can claim the request. A request starts unbound until the user opens https://granitegranted.com/consent/{id} signed in to Granite. Once they claim one, your later requests with the same x-user-id are bound when you file them.
# your app credential + the user's ChirpAuth sub, as your app received it curl https://granitegranted.com/v1/approval-requests \ -H "Authorization: Bearer $GRANITE_APP_SECRET" \ -H "x-granite-app-id: atlas" \ -H "x-user-id: sub_8Q3K2M11XZ" \ -H "Content-Type: application/json" \ -d '{ "request_type": "storage_grant", "title": "Read your project folder", "summary": "Atlas wants read access to one folder.", "risk_level": "medium", "requested_action": "read files", "requested_resource": "storage:drive:folder_123", "requested_scopes": ["drive.read"], "callback_url": "https://atlas.example/granite/callback" }'
// abridged { "id": "2c8be6d4-d657-48ef-b3f5-923c4ed975df", "owner_uid": null, // unbound until the user claims it "owner_bound": false, "requester_user_ref": "sub_8Q3K2M11XZ", // your x-user-id, echoed "requester_app_id": "atlas", // from your credential, not the body "request_type": "storage_grant", "risk_level": "medium", "status": "pending", "created_at": "2026-06-09T14:21:07Z" }
Learn the outcome
When the user decides, Granite POSTs to your callback URL (register a callback_secret and the body is HMAC-signed). Delivery is a single attempt with no retry, so treat the webhook as a nudge and poll as the source of truth. Polling reads the request as its requester: only the app that filed it can read it this way, whoever owns it, and x-user-id does not change what you can see. Only the user can decide it, on their own Granite session.
{
"event": "approval.decided",
"request_id": "2c8be6d4-d657-48ef-b3f5-923c4ed975df",
"decision": "approved",
"status": "approved",
"requester_user_ref": "sub_8Q3K2M11XZ",
"requester_app_id": "atlas",
"trace_id": null,
"decided_at": "2026-06-09T14:23:40Z"
}
curl https://granitegranted.com/v1/approval-requests/2c8be6d4-d657-48ef-b3f5-923c4ed975df \ -H "Authorization: Bearer $GRANITE_APP_SECRET" \ -H "x-granite-app-id: atlas" \ -H "x-user-id: sub_8Q3K2M11XZ" # → { "status": "approved", "decision_id": "…", "grant_id": "…", ... } # grant_id is there only if the approval created a grant; a one_time_action has none
Verify before acting
A standing grant can be revoked by the user at any time. Verify it is still active immediately before you rely on it — do not cache the answer. A one_time_action creates no grant: verify it by request_id and name your app, { "request_id": "…", "subject_app_id": "atlas" }.
curl https://granitegranted.com/v1/grants/verify \ -H "Authorization: Bearer $GRANITE_APP_SECRET" \ -H "x-granite-app-id: atlas" \ -H "x-user-id: sub_8Q3K2M11XZ" \ -H "Content-Type: application/json" \ -d '{ "grant_id": "7c01…", "resource": "storage:drive:folder_123", "scopes": ["drive.read"] }'
{
"approved": true,
"grant": {
"id": "7c01…",
"status": "active",
"scopes": ["drive.read"]
}
}
# revoked / expired / scope-mismatch → { "approved": false, "reason": "…" }
Identity you receive
requester_user_ref you sent, the decision, the granted scopes, and your trace_id echoed back. Once the user claims a request, reading it and verifying its grant also return owner_uid: Granite's id for the person, the same for every app they use through Granite. Key your records on requester_user_ref.owner_uid.Get credentials
Credentials are issued by hand while Granite is invitation-stage. Email support@granitegranted.com to register your app — there is no self-serve signup yet; a person reads and replies.