# Outbound webhooks

_Register a URL, receive HMAC-signed callbacks on envelope state events, verify the signature, react._

_Last updated: 2026-05-07_

---

# Outbound webhooks

Webhooks are how the API tells you about state changes that happen between your calls. Register a URL, the wrapper POSTs to it whenever a subscribed event fires, your handler verifies the HMAC signature and reacts.

This page covers the customer-side outbound webhooks (the API calling your URL). It does not cover inbound webhooks the wrapper handles internally (Documenso → wrapper, Relaystation → wrapper); those don't surface to API consumers.

## Registering a webhook

```sh
curl -X POST https://api.verafirma.com/v1/webhooks \
  -H "Authorization: Bearer vf_live_..." \
  -H "Content-Type: application/json" \
  -d '[component]'
```

Response (`201`):

```json
[component]
```

**The `secret` field is returned ONCE.** Subsequent reads of the registration omit it. Store it on registration; if it's lost, delete the registration and re-register.

## V1 events

The seven events the wrapper emits:

| Event | When |
|---|---|
| `envelope.sent` | Envelope distributed; signing-link emails sent. |
| `envelope.viewed` | A recipient opened the signing link. |
| `envelope.signed` | A `SIGNER` recipient completed their signature. Fires once per signer. |
| `envelope.completed` | All `SIGNER` recipients have signed; envelope is terminal at `COMPLETED`. |
| `envelope.rejected` | A `SIGNER` rejected; envelope is terminal at `REJECTED`. |
| `envelope.expired` | Signing window closed without all signers; envelope is terminal at `EXPIRED`. |
| `envelope.cancelled` | Envelope cancelled by the customer or an operator; envelope is terminal at `CANCELLED`. |

Subscribe with `events: ["*"]` to receive all events. Subscribe with a narrower list to receive only those.

## Delivery shape

Each delivery is a POST with a JSON body and signing headers:

```http
POST /webhooks/verafirma HTTP/1.1
Content-Type: application/json
X-Verafirma-Signature: t=<unix-ts>,v1=<hex-hmac>
X-Verafirma-Event: envelope.completed
X-Verafirma-Delivery-Id: dlv_<uuid>

[component]
}
```

The body is the same shape `GET /v1/envelopes/id` would return for that envelope, plus the event metadata at the top level.

## Verifying signatures

The `X-Verafirma-Signature` header carries an HMAC-SHA256 over `$timestamp.$raw_body`, signed with the registration secret. Verify before trusting the body.

```js

function verifyWebhook(rawBody, signatureHeader, secret) [component]
```

The `t=` timestamp is unix seconds. Including it in the signed payload prevents an attacker who captures one delivery from replaying it later.

The 5-minute freshness window above is a recommended client-side check; the API doesn't enforce it.

## Retry behavior

On non-2xx responses (or no response within the request timeout), the delivery is queued for retry. The default retry schedule:

```
1m → 5m → 15m → 1h → 6h → 24h → done (terminal failure logged)
```

The schedule is configurable on the operator side; the cap on retries is too. If your handler returns 2xx within the schedule, the delivery is marked successful; otherwise it transitions to terminal failure and stops retrying.

After enough consecutive failures the registration's `consecutiveFailures` counter reaches a threshold and the operator can choose to disable the registration. Successful deliveries reset the counter to zero.

## Idempotency on your side

The `X-Verafirma-Delivery-Id` header is unique per delivery. Two deliveries with the same id are rare (network blips at the SQS layer can occasionally produce them) but possible.

The recommended pattern: persist the delivery id when you process it; on a future delivery with the same id, return 2xx without re-running the side effect. This is a one-row, one-index pattern — cheap, prevents duplicate downstream actions.

## Managing registrations

```sh
# List your registrations:
GET /v1/webhooks

# Read a single registration (no secret in the response):
GET /v1/webhooks/id

# Update events list, URL, description, or active flag:
PUT /v1/webhooks/id

# Delete (deliveries history retained for audit):
DELETE /v1/webhooks/id

# Page through delivery history:
GET /v1/webhooks/id/deliveries?limit=&offset=&status=
```

The `status` filter on the deliveries list is one of `PENDING`, `SUCCESS`, `FAILED`. Use the deliveries log when debugging why your handler isn't seeing an event you expected.

## What NOT to do

- **Don't trust the body without verifying the signature.** A handler that processes the body without signature verification is the standard webhook footgun; an attacker who knows your URL can POST anything they want.
- **Don't store the secret in client-side code.** It's a server-side credential. If your URL is reachable from the client side, your handler still runs server-side; keep the secret there.
- **Don't return 2xx until the side effect is durable.** A 2xx response tells the wrapper "delivery acknowledged"; if your downstream side effect failed but you returned 2xx anyway, the wrapper won't retry.
- **Don't poll instead of subscribing.** `GET /v1/envelopes/id/status` is for ad-hoc checks, not for pub-sub. Webhooks scale; polling doesn't.
