Grantex can POST a signed JSON payload to your server whenever key events occur — grant created, grant revoked, or token issued. Use webhooks to keep your application in sync with the authorization lifecycle without polling.
Supported Events
| Event | When it fires |
|---|
grant.created | A new grant is issued (user completes consent flow) |
grant.revoked | A grant is revoked (root or cascade) |
token.issued | A token is issued (same moment as grant.created for initial exchange) |
Registering an Endpoint
curl -X POST https://api.grantex.dev/v1/webhooks \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/grantex",
"events": ["grant.created", "grant.revoked"]
}'
Response:
{
"id": "wh_01JXYZ...",
"url": "https://your-app.com/webhooks/grantex",
"events": ["grant.created", "grant.revoked"],
"secret": "a3f8c2...",
"createdAt": "2026-02-26T12:00:00Z"
}
The secret is returned only once. Store it securely — you need it to verify incoming payloads.
Event Payload Shape
Every webhook POST has the same envelope:
{
"id": "evt_01JXYZ...",
"type": "grant.created",
"createdAt": "2026-02-26T12:00:00Z",
"data": { ... }
}
grant.created
{
"grantId": "grnt_01...",
"agentId": "ag_01...",
"principalId": "user-123",
"scopes": ["calendar:read"],
"expiresAt": "2026-03-26T12:00:00Z"
}
grant.revoked
{
"grantId": "grnt_01...",
"cascade": true
}
cascade: true means descendant grants were also revoked.
token.issued
{
"tokenId": "tok_01...",
"grantId": "grnt_01...",
"agentId": "ag_01...",
"principalId": "user-123",
"scopes": ["calendar:read"],
"expiresAt": "2026-03-26T12:00:00Z"
}
Verifying Signatures
Every request includes an X-Grantex-Signature header with a hex-encoded HMAC-SHA256 signature of the raw request body:
X-Grantex-Signature: sha256=<hex>
import { verifyWebhookSignature } from '@grantex/sdk';
app.post('/webhooks/grantex', (req, res) => {
const sig = req.headers['x-grantex-signature'] as string;
const rawBody = req.rawBody;
if (!verifyWebhookSignature(rawBody, sig, process.env.GRANTEX_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
console.log('Received event:', event.type);
res.status(200).send();
});
Always verify signatures before trusting the payload. Use the raw request body — not the parsed JSON.
SDK Usage
import { Grantex } from '@grantex/sdk';
const grantex = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });
// Register
const endpoint = await grantex.webhooks.create({
url: 'https://your-app.com/webhooks/grantex',
events: ['grant.created', 'grant.revoked', 'token.issued'],
});
console.log('Webhook secret:', endpoint.secret);
// List
const { webhooks } = await grantex.webhooks.list();
// Delete
await grantex.webhooks.delete(endpoint.id);
Delivery Behaviour
- Grantex delivers webhooks with a 10-second timeout per request.
- Your endpoint should return any 2xx status to be considered successful.
- Respond quickly (under 5s) and do heavy processing asynchronously.
- Failed deliveries are retried with exponential backoff.
Local Development
Use a tunnel tool to expose your local server:
# ngrok
ngrok http 3000
# Register the tunnel URL
curl -X POST http://localhost:3001/v1/webhooks \
-H "Authorization: Bearer dev-api-key-local" \
-H "Content-Type: application/json" \
-d '{"url":"https://<your-ngrok-id>.ngrok.io/webhooks","events":["grant.created"]}'