> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.vouch.careers/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.vouch.careers/_mcp/server.

# Getting Started

Vouch sends webhook events to the URL you configure in **Settings → Webhooks**. Each request is a `POST` with a JSON body and an `x-vouch-signature` header you should verify before trusting the payload. See the **Webhooks** section of the API reference for the exact shape of each event's payload.

## Verifying the signature

Vouch signs every webhook request with HMAC-SHA256, keyed by the webhook secret shown when you create the webhook. The signature is sent in the `x-vouch-signature` header in the form:

```
t=<unix timestamp>,v1=<hex hmac of "{t}.{raw body}">
```

Verify it by recomputing the HMAC over `{timestamp}.{raw request body}` using your webhook secret, and comparing it to `v1` with a constant-time comparison. Reject requests where the timestamp is more than 5 minutes old, to guard against replay attacks.

## Example (Node.js)

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

function verifyWebhookSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => {
      const [k, ...rest] = part.split('=');
      return [k.trim(), rest.join('=').trim()];
    })
  );
  const timestamp = Number(parts.t);
  const expected = parts.v1;
  if (!timestamp || !expected) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) {
    return false;
  }
  const signedPayload = `${timestamp}.${rawBody}`;
  const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  } catch {
    // Mismatched length (e.g. a malformed or tampered v1 value) throws rather
    // than returning false — treat that the same as "doesn't match".
    return false;
  }
}

// Usage — rawBody MUST be the exact raw request body string/bytes you
// received, never a value you've re-JSON.stringify'd after parsing it —
// key ordering and whitespace aren't guaranteed to round-trip identically
// to what was actually signed, so a re-serialized body can fail verification
// even for a legitimate request. If you're on Express with express.json(),
// capture the raw body via a `verify` callback (or express.raw()) before
// the JSON parser consumes it — req.body is not the same bytes that were signed.
const isValid = verifyWebhookSignature(
  request.rawBody,
  request.headers['x-vouch-signature'],
  process.env.WEBHOOK_SECRET
);
```

> **Important:** Always use `crypto.timingSafeEqual` (or an equivalent constant-time comparison) when verifying signatures, to prevent timing attacks — and always wrap it in a try/catch, since it throws (rather than returning `false`) when the two buffers differ in length.