> ## 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 carries a timestamped signature over `<timestamp>.<raw body>`:

```
X-Grantex-Timestamp: <unix seconds>
X-Grantex-Signature-V2: sha256=<hex>
X-Grantex-Signature: sha256=<hex>      # legacy, see below
```

Verify against `X-Grantex-Signature-V2` and reject deliveries older than your
tolerance (300 seconds by default). Both halves matter: the timestamp is inside
the signed material, so it cannot be rewritten, and without it a captured
delivery would stay valid forever.

Each delivery attempt is signed at the moment it is sent, so a retry arriving
after backoff carries a fresh timestamp rather than the time the event was
queued.

Always verify against the **raw** request body. A body that has been parsed and
re-serialized will not hash to the same value.

**TypeScript:**

```typescript theme={null}
import { verifyWebhook } from '@grantex/sdk';

app.post('/webhooks/grantex', (req, res) => {
  const rawBody = req.rawBody; // raw string/Buffer, not parsed JSON

  const ok = verifyWebhook({
    payload: rawBody,
    signature: req.headers['x-grantex-signature-v2'] as string,
    timestamp: req.headers['x-grantex-timestamp'] as string,
    secret: process.env.GRANTEX_WEBHOOK_SECRET!,
  });
  if (!ok) 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
import os

@app.route('/webhooks/grantex', methods=['POST'])
def handle_webhook():
    raw_body = request.get_data(as_text=True)

    ok = verify_webhook(
        raw_body,
        request.headers.get('X-Grantex-Signature-V2', ''),
        request.headers.get('X-Grantex-Timestamp', ''),
        os.environ['GRANTEX_WEBHOOK_SECRET'],
    )
    if not ok:
        return 'Invalid signature', 401

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

**Go:**

```go theme={null}
ok := grantex.VerifyWebhook(
    rawBody,
    r.Header.Get("X-Grantex-Signature-V2"),
    r.Header.Get("X-Grantex-Timestamp"),
    os.Getenv("GRANTEX_WEBHOOK_SECRET"),
    grantex.DefaultWebhookToleranceSeconds,
)
if !ok {
    http.Error(w, "Invalid signature", http.StatusUnauthorized)
    return
}
```

### Legacy `X-Grantex-Signature`

The original header signs the body alone. Because it commits to nothing
time-bound, a delivery captured once stays valid forever and can be replayed at
any time. It is still sent so existing receivers keep working, and
`verifyWebhookSignature` / `verify_webhook_signature` /
`VerifyWebhookSignature` still verify it — but they are deprecated. Move to the
timestamped header; the legacy one will be withdrawn.

## 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"]}'
```

## Ownership

Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.