# Guide to VGS (Very Good Security) Webhooks: Features and Best Practices

VGS webhook notifications tell you when something changes in your VGS (Very Good Security) organisation: a vault route is created, changed or deleted, a user logs in or changes their MFA, or the proxy starts hitting upstream errors. If you use VGS to tokenise and proxy sensitive data, these notifications give your security and platform teams an audit trail and early warning, without polling the dashboard.

This guide covers how VGS notifications are set up and signed, the events available, what VGS does and doesn't document about the payload, and the practices that keep a VGS receiver reliable. VGS's payment products have their own notifications, documented separately, and are not covered here.

## What are VGS webhooks?

VGS sends webhook notifications for events in your organisation. You add an HTTPS endpoint in the VGS Dashboard, choose events, and choose the sources they apply to: tenants, users or organisations, each marked as sandbox or live. Notifications are available to activated organisations.

Each request carries a `vgs-signature` header of the form `t=<timestamp>,v0=<signature>`. The signature is a hex HMAC-SHA256 of the timestamp, a period and the raw body, keyed with a secret VGS generates for each webhook integration. VGS's prose calls it "a sha256 hash of a raw request body", but its code sample shows the full construction, with the timestamp included.

Some frequent events are grouped. For `proxy.upstream_error` and `alias.reveal_failed`, VGS sends only the 1st, 10th, 100th, 1000th and so on within a 5-minute window, with an `occurrence` count of how many were folded together.

## VGS webhook features

| Feature | Details |
| --- | --- |
| Availability | Activated organisations |
| Configuration | VGS Dashboard, **Organization Settings > Notifications** |
| Subscription | Events plus sources (tenants, users, organisations; sandbox or live) |
| Signature header | `vgs-signature: t=<unix seconds>,v0=<hex>` |
| Scheme | HMAC-SHA256, hex |
| Signed content | `<t>.<raw body>` |
| Secret | One per webhook integration, generated by VGS |
| Suggested window | 60 seconds in VGS's sample, which VGS calls "very strict" |
| Attempts | 8: immediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours |
| After the last attempt | The delivery is cancelled |
| Disabled endpoints | Notifications generated while disabled are not retried |
| Grouping | `proxy.upstream_error` and `alias.reveal_failed`, per 5-minute window |
| Replay | On request through VGS support |
| Limit | 20 integrations per organisation by default |
| Source IPs | `44.228.126.217`, `50.112.21.217`, `52.24.126.164`, `54.148.139.208`, `54.164.207.221`, `54.90.7.123`, `2600:1f24:64:8000::/52`, `2600:1f28:37:4000::/52` |

## Common events

VGS lists 12 events. For each one it names the details the notification carries.

| Event | Scope | Details |
| --- | --- | --- |
| `route.created` | Vault | `route_id`, `route_name`, `environment` |
| `route.updated` | Vault | `route_id`, `route_name`, `environment` |
| `route.delete` | Vault | `route_id`, `route_name`, `environment` |
| `vault.created` | Organisation | `vault_id`, `vault_name`, `environment` |
| `user.permissions_updated` | User | `user_email`, `org_id`, list of permissions |
| `user.permissions_deleted` | User | `user_email`, `org_id`, list of permissions |
| `user.logged_in` | User | `user_ip`, `user_email` |
| `user.password_updated` | User | `user_ip`, `user_email` |
| `user.mfa_created` | User | `user_ip`, `user_email` |
| `user.mfa_deleted` | User | `user_ip`, `user_email` |
| `proxy.upstream_error` | Vault | `upstream_host`, `upstream_error`, `environment` (grouped) |
| `alias.reveal_failed` | Vault | `environment`, `storage` (grouped) |

Note that the route deletion event is `route.delete`, not `route.deleted`. VGS's grouping paragraph also mentions `record.reveal_failed`, which is not in the table, so it is unclear whether that is an older name for `alias.reveal_failed`.

VGS does not publish an example payload, so the field that carries the event name, the body's structure and even the content type are not documented. Capture a few real notifications, for example on a Hookdeck source, before you write parsing code, and code defensively around missing fields.

## Setting up VGS webhooks

1. In the VGS Dashboard, go to **Organization Settings > Notifications**. If your organisation is not enabled for notifications, the page tells you to contact support.
2. Click **Create new** and enter your HTTPS endpoint URL.
3. Choose the events, and for each the sources it applies to.
4. Save, and store the integration's secret server-side, for example as `VGS_WEBHOOK_SECRET`.

From the Notifications page you can open a notification to edit it, delete it, or switch it between active and inactive. Anything generated while it is inactive is not delivered later.

## Securing VGS webhooks

Split `vgs-signature` on commas and read `t` and `v0`. Build `<t>.<raw body>`, compute an HMAC-SHA256 with your secret, hex-encode it, and compare it with `v0` in constant time.

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

function verifyVgsSignature(rawBody, header, secret) {
  if (!header || !secret) return false;
  const parts = {};
  for (const item of header.split(',')) {
    const i = item.indexOf('=');
    if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
  }
  if (!/^\d+$/.test(parts.t ?? '') || !parts.v0) return false;

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

const app = express();

app.post('/webhooks/vgs', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.VGS_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyVgsSignature(req.body, req.get('vgs-signature'), secret)) {
    return res.sendStatus(401);
  }

  // Optional: compare t with the current time. Retries keep the original t,
  // so a 60-second window rejects every retry after the first minute.

  // Store the raw body, then process asynchronously.
  res.sendStatus(200);
});

```

The same check in Python, based on VGS's own sample but with a constant-time comparison and the timestamp check left optional:

```python
import hashlib
import hmac
import re
import time

def verify_vgs_signature(raw_body: bytes, header, secret: str, tolerance_seconds: int = 0) -> bool:
    if not header or not secret:
        return False
    parts = dict(item.strip().split("=", 1) for item in header.split(",") if "=" in item)
    timestamp, signature = parts.get("t"), parts.get("v0")
    if not timestamp or not re.fullmatch(r"[0-9]+", timestamp) or not signature:
        return False
    if tolerance_seconds > 0 and abs(time.time() - int(timestamp)) > tolerance_seconds:
        return False
    expected = hmac.new(
        secret.encode("utf-8"), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature.lower().encode("utf-8"), expected.encode("utf-8"))

```

VGS's sample compares with `!=`, which is not constant-time, and applies a 60-second window before checking the signature. VGS itself points out that retries are sent with the same timestamp, so "a retry that happens after 30 minutes will be rejected". If you keep a window, widen it to cover the retry schedule, or rely on the signature alone.

> **Make VGS webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `vgs-signature` header against the timestamp and raw body without a timestamp window, answers VGS with an immediate `200 OK`, and durably queues every route, user and proxy notification before your service sees it.

## VGS webhook limitations and pain points

### The suggested window rejects retries

**The Problem:** Retried notifications fail verification, so an outage turns into lost events.

**Why It Happens:** VGS's sample rejects timestamps more than 60 seconds old, and every retry carries the original timestamp. The retry schedule runs for more than a day, so almost every retry falls outside the window.

**Workarounds:**

* Verify the signature without a timestamp check, or use a window that covers the full schedule.
* Track which notifications you have processed to protect against replays instead.

**How Hookdeck Can Help:** Hookdeck's VGS source type checks the signature without enforcing a timestamp window, so retries and replays verify.

### No documented payload

**The Problem:** You don't know the exact shape of the body until you receive one.

**Why It Happens:** VGS lists the details each event carries and mentions an `occurrence` field for grouped events, but publishes no example, envelope, HTTP method or content type.

**Workarounds:**

* Capture real notifications for each event you subscribe to before writing parsers.
* Store the raw body so you can reprocess it once your parser is right.

**How Hookdeck Can Help:** Hookdeck stores every notification with its headers and body, so you can inspect real payloads and replay them through your parser after you fix it.

### Grouped events undercount

**The Problem:** Your error counts don't match what happened at the proxy.

**Why It Happens:** For `proxy.upstream_error` and `alias.reveal_failed`, VGS sends only the 1st, 10th, 100th, 1000th notification in each 5-minute window.

**Workarounds:**

* Read the `occurrence` count rather than counting deliveries.
* Treat these events as alerts that something is wrong, and go to VGS logs for detail.

**How Hookdeck Can Help:** Hookdeck filters can route grouped error events to an alerting destination and audit events to a separate store.

### Nothing is retried for a disabled endpoint

**The Problem:** Events from a maintenance window never arrive.

**Why It Happens:** If you disable a notification endpoint, VGS does not retry anything generated while it was disabled. Recovering those events means asking VGS support for a replay.

**Workarounds:**

* Keep endpoints enabled during maintenance and let your own receiver hold events.
* If you do disable one, request a replay with the time range and event types.

**How Hookdeck Can Help:** With a Hookdeck source as the endpoint, you can pause delivery to your own service in Hookdeck while VGS keeps sending, then resume and receive everything that arrived.

## Best practices

### Verify against the raw body

The signature covers the exact bytes after the timestamp. Use `express.raw()` in Express, `await request.text()` in a Next.js route handler and `await request.body()` in FastAPI.

### Allowlist the published IPs

VGS lists the addresses notifications come from, including two IPv6 ranges. Allowlisting them adds a layer, but keep verifying the signature.

### Return exactly `200`

VGS treats "non-200 responses" as failures. Return `200` once the notification is verified and stored.

### Watch login and permission events closely

`user.logged_in`, `user.mfa_deleted` and the permission events are the ones a security team cares about. Route them to alerting, not only to a log.

### Process asynchronously

Verify, store, return `200`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

VGS signs each notification with `vgs-signature: t=...,v0=...`, a hex HMAC-SHA256 of the timestamp and raw body under the integration's secret. Verify against the raw bytes, don't apply the sample's 60-second window to retries that keep their original timestamp, read `occurrence` on grouped events, and capture real payloads before you parse them, because VGS doesn't publish one.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks every VGS signature, and holds each notification until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle VGS webhooks reliably in minutes.