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 your own requester_user_ref — never their email, never a shared identifier.
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. App-filed requests start unbound: Granite binds one to the user when they sign in to decide — pairwise identity means you and Granite name the same person differently, by design.
# your app credential + your own pairwise id for the user curl https://granitegranted.com/v1/approval-requests \ -H "Authorization: Bearer $GRANITE_APP_SECRET" \ -H "x-granite-app-id: atlas" \ -H "x-user-id: u_8Q3K2M11XZ" \ -H "Content-Type: application/json" \ -d '{ "request_type": "delegation_grant", "title": "Act on your behalf", "summary": "Atlas wants to let other agents act for you.", "risk_level": "high", "requested_action": "let other agents act on your behalf", "requested_resource": "delegation", "requested_scopes": ["read", "send", "re-delegate"], "callback_url": "https://atlas.example/granite/webhook" }'
{
"id": "2c8be6d4-d657-48ef-b3f5-923c4ed975df",
"owner_uid": null, // unbound until the user signs in
"requester_user_ref": "u_8Q3K2M11XZ", // your x-user-id, echoed
"requester_app_id": "atlas", // bound by Granite, not you
"request_type": "delegation_grant",
"risk_level": "high",
"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 fire-and-forget today, so treat the webhook as a nudge and poll as the source of truth.
{
"event": "approval.decided",
"request_id": "2c8be6d4-d657-48ef-b3f5-923c4ed975df",
"decision": "approved",
"status": "approved",
"requester_user_ref": "u_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: u_8Q3K2M11XZ" # → { "status": "approved", "grant_id": "…", ... }
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.
curl https://granitegranted.com/v1/grants/verify \ -H "Authorization: Bearer $GRANITE_APP_SECRET" \ -H "x-granite-app-id: atlas" \ -H "x-user-id: u_8Q3K2M11XZ" \ -d '{ "grant_id": "7c01…", "resource": "delegation", "scopes": ["send"] }'
{
"approved": true,
"grant": {
"id": "7c01…",
"status": "active",
"scopes": ["read", "send", "re-delegate"]
}
}
# revoked / expired / scope-mismatch → { "approved": false, "reason": "…" }
Identity you receive
requester_user_ref, the decision, granted scopes, and a trace id for the record.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.