> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications for grant lifecycle events.

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

```bash theme={null}
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:**

```json theme={null}
{
  "id": "wh_01JXYZ...",
  "url": "https://your-app.com/webhooks/grantex",
  "events": ["grant.created", "grant.revoked"],
  "secret": "a3f8c2...",
  "createdAt": "2026-02-26T12:00:00Z"
}
```

<Warning>
  The `secret` is returned only once. Store it securely — you need it to verify incoming payloads.
</Warning>

## Event Payload Shape

Every webhook POST has the same envelope:

```json theme={null}
{
  "id": "evt_01JXYZ...",
  "type": "grant.created",
  "createdAt": "2026-02-26T12:00:00Z",
  "data": { ... }
}
```

### `grant.created`

```json theme={null}
{
  "grantId": "grnt_01...",
  "agentId": "ag_01...",
  "principalId": "user-123",
  "scopes": ["calendar:read"],
  "expiresAt": "2026-03-26T12:00:00Z"
}
```

### `grant.revoked`

```json theme={null}
{
  "grantId": "grnt_01...",
  "cascade": true
}
```

`cascade: true` means descendant grants were also revoked.

### `token.issued`

```json theme={null}
{
  "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>
```

<CodeGroup>
  ```typescript TypeScript theme={null}
  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();
  });
  ```

  ```python Python theme={null}
  from grantex import verify_webhook_signature
  import os

  @app.route('/webhooks/grantex', methods=['POST'])
  def handle_webhook():
      sig = request.headers.get('X-Grantex-Signature', '')
      raw_body = request.get_data(as_text=True)

      if not verify_webhook_signature(raw_body, sig, os.environ['GRANTEX_WEBHOOK_SECRET']):
          return 'Invalid signature', 401

      event = request.get_json()
      print('Received event:', event['type'])
      return '', 200
  ```
</CodeGroup>

<Note>
  Always verify signatures before trusting the payload. Use the raw request body — not the parsed JSON.
</Note>

## SDK Usage

<CodeGroup>
  ```typescript TypeScript theme={null}
  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);
  ```

  ```python Python theme={null}
  from grantex import Grantex

  client = Grantex(api_key=os.environ['GRANTEX_API_KEY'])

  # Register
  endpoint = client.webhooks.create(
      url='https://your-app.com/webhooks/grantex',
      events=['grant.created', 'grant.revoked', 'token.issued'],
  )
  print('Webhook secret:', endpoint.secret)

  # List
  result = client.webhooks.list()

  # Delete
  client.webhooks.delete(endpoint.id)
  ```
</CodeGroup>

## 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:

```bash theme={null}
# 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"]}'
```
