# VGS Webhooks

## When to Use This Skill

* How do I receive VGS (Very Good Security) webhooks?
* How do I verify the `vgs-signature` header?
* How do I handle `route.updated`, `vault.created`, or `user.logged_in` events?
* Why is my VGS webhook signature verification failing, or why are retries rejected?

## Essential Code (USE THIS)

VGS sends a `vgs-signature` header on every notification:

```
vgs-signature: t=1623691785,v0=9370aa8513da0a14d21478f11f039708f7e664bfffebb939f574e57818ce08de

```

* `t`: UTC Unix timestamp in seconds
* `v0`: HMAC-SHA256 (lowercase hex) of `"<t>.<raw body>"`, keyed with the per-integration secret as UTF-8

VGS retries reuse the **original** `t`, and Hookdeck retries and replays do too. The VGS docs example rejects timestamps older than 60 seconds, which would reject any retry. The examples therefore skip the freshness check by default; set `VGS_TIMESTAMP_TOLERANCE_SECONDS` to a positive number to opt in.

### Verification (Node.js)

```javascript
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw, not express.json)
function verifyVgs(rawBody, signatureHeader, secret, toleranceSeconds = 0) {
  if (!signatureHeader || !secret) return false;
  const parts = {};
  for (const kv of signatureHeader.split(',')) {
    const i = kv.indexOf('=');
    if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();
  }
  if (!parts.t || !parts.v0) return false;
  if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;

  const expected = crypto.createHmac('sha256', secret)
    .update(`${parts.t}.`).update(rawBody).digest('hex');
  const a = Buffer.from(parts.v0), b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

```

### Verification (Python)

```python
import hashlib, hmac, time

def verify_vgs(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 0) -> bool:
    if not signature_header or not secret:
        return False
    parts = dict(kv.strip().split("=", 1) for kv in signature_header.split(",") if "=" in kv)
    if "t" not in parts or "v0" not in parts:
        return False
    if tolerance > 0 and abs(time.time() - int(parts["t"])) > tolerance:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts["v0"], expected)

```

VGS has no SDK for webhook verification, so the manual HMAC above is the supported approach in every framework.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vgs-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vgs-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vgs-webhooks/examples/fastapi/).

## Common Event Types

The event name is the top-level `event` field in the JSON body (there is no event header).

| Event | Scope | Description |
| --- | --- | --- |
| `route.created` | Vault | A route was created |
| `route.updated` | Vault | A route was changed |
| `route.delete` | Vault | A route was deleted |
| `vault.created` | Organization | A vault was created |
| `user.permissions_updated` | User | A user's permissions changed |
| `user.permissions_deleted` | User | A user's permissions were removed |
| `user.logged_in` | User | A user logged in |
| `user.password_updated` | User | A user changed their password |
| `user.mfa_created` | User | A user added MFA |
| `user.mfa_deleted` | User | A user removed MFA |
| `proxy.upstream_error` | Vault | Proxy upstream error (grouped; see `occurrence`) |
| `alias.reveal_failed` | Vault | Alias reveal failed (grouped; see `occurrence`) |

> **For the full event reference**, see [VGS webhook notifications](https://docs.verygoodsecurity.com/enterprise-platform/developer-resources/webhook-notifications).

## Important Headers

| Header | Description |
| --- | --- |
| `vgs-signature` | `t=<unix seconds>,v0=<hex HMAC-SHA256>` |

## Environment Variables

```bash
VGS_WEBHOOK_SECRET=your_integration_secret   # unique secret generated for each webhook integration
VGS_TIMESTAMP_TOLERANCE_SECONDS=0            # optional; a positive value opts in to the freshness check

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 vgs --path /webhooks/vgs

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vgs-webhooks/references/overview.md) - VGS webhook concepts, events, delivery schedule
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vgs-webhooks/references/setup.md) - Dashboard configuration
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vgs-webhooks/references/verification.md) - Signature verification details