> ## Documentation Index
> Fetch the complete documentation index at: https://visaflo.ca/knowledge-base/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> VisaFlo posts to your address when an agreement is sent, opened, signed by a signer and fully signed.

Set the address in **Settings** > **API** > **Webhook**. Use **Send test** to see an event arrive before you build on it.

## Events

| Event | When |
| - | - |
| `agreement_sent` | The invitation went out, from VisaFlo or from the API |
| `agreement_viewed` | A signer opened it for the first time |
| `agreement_signer_signed` | A signer finished signing |
| `agreement_signed` | Everyone, the firm included, has signed. The certificate is ready. |

Every event has the same body: the agreement as `GET /agreement/{agreementId}` returns it, plus `id`, `event` and `timestamp`. Signer events add `signer`. `agreement_signed` adds `signatureId`, `round` and the `invoices` it created. Your `metadata` is in all of them, so you can find your own record (a booking, a Monday item) without a lookup.

```json theme={null}
{
  "id": "evt_…",
  "event": "agreement_signed",
  "timestamp": "2026-10-01T15:04:05.000Z",
  "agreementId": "VFLO_AGREEMENT_ID",
  "status": "completed",
  "metadata": { "booking_id": "savvycal-8842" },
  "certificateUrl": "https://api.vflo.app/api/public/agreement/VFLO_AGREEMENT_ID/certificate"
}
```

## Check the signature

Every post carries:

* `X-VisaFlo-Signature: t=<unix seconds>,v1=<hex>`
* `X-VisaFlo-Event` and `X-VisaFlo-Delivery`

`v1` is the HMAC-SHA256, with your signing secret (shown in Settings > API, starts with `whsec_`), of the string `<t>.<raw body>`. Compute it over the raw body, compare in constant time, and reject a `t` that is more than five minutes old.

```js theme={null}
const crypto = require("crypto");

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
```

## Delivery

* Answer with any `2xx` within 10 seconds. Anything else is a failure.
* A failed post is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours (seven tries in all). After that it is marked failed; **Settings** > **API** lists the latest deliveries and can send one again.
* An event can arrive more than once. Use `X-VisaFlo-Delivery`, or the agreement's `status`, to ignore repeats.
* Addresses must be `https://` and public. Redirects are not followed.
* **New secret** replaces the signing secret at once. Update your receiver first, or accept both for a while.


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