---
title: Webhooks
description: Receive, verify, and reliably process workspace events
group: Guides
---

# Webhooks

# Webhooks

Webhooks notify your application when something changes in Verifycate. Instead
of repeatedly polling the API, your server receives an HTTPS request shortly
after a certificate, recipient, request, correction, or engagement event.

Use webhooks to update an internal learner record, trigger a downstream
workflow, build an audit trail, or measure recipient engagement. Webhooks
require an active Pro seat.

## How it works

1. Configure one or more HTTPS endpoints in **Settings → Webhooks**
   or with [Create a webhook](/docs/api/webhooks/create).
2. Choose individual event types, or subscribe to `*` to receive every event.
3. Verify the signature of every request before parsing or acting on it.
4. Process the event idempotently and return any HTTP `2xx` status promptly.
5. Use [List deliveries](/docs/api/webhooks/deliveries) to investigate retries
   and failures.

Verifycate sends requests with these headers:

| Header                   | Value                             |
| ------------------------ | --------------------------------- |
| `Content-Type`           | `application/json`                |
| `User-Agent`             | `Verifycate-Webhook/1.0`          |
| `X-Verifycate-Signature` | Hex-encoded HMAC-SHA256 signature |

## Payload

Every delivery is a JSON POST:

```json
{
  "id": "7c8e0b2a-3d1f-4c55-9a11-2f6b8d4e1c90",
  "event_type": "certificate.issued",
  "occurred_at": "2026-09-03T12:00:00Z",
  "workspace_id": "550e8400-e29b-41d4-a716-446655440000",
  "certificate_id": "8ac68e9d-4aca-43ca-88f0-f2220f849626",
  "issue_id": "1069340d-5b74-4670-a319-41b8fe3a0e5d",
  "data": {
    "recipient_email": "ada@example.com"
  }
}
```

| Field            | Meaning                                               |
| ---------------- | ----------------------------------------------------- |
| `id`             | Stable event identifier; use it as an idempotency key |
| `event_type`     | The event that occurred                               |
| `occurred_at`    | When the event was recorded, as an RFC 3339 timestamp |
| `workspace_id`   | Workspace that owns the event                         |
| `certificate_id` | Related certificate when the event has one            |
| `issue_id`       | Related certificate issue when the event has one      |
| `data`           | Event-specific data; it may be an empty object        |

Do not assume deliveries arrive in order. A retry for an older event can arrive
after a newer event.

## Verify the signature

Each endpoint has a secret returned when it is created. Store the secret in a
secret manager; do not commit it or expose it to browser code.

Calculate HMAC-SHA256 over the exact raw request bytes using the endpoint
secret, hex-encode the digest, and compare it with
`X-Verifycate-Signature` using a constant-time comparison. Verify the signature
before decoding JSON—the parsed and re-serialized body may not have the same
bytes.

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyVerifycateWebhook(rawBody, receivedSignature, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const expectedBytes = Buffer.from(expected, "hex");
  const receivedBytes = Buffer.from(receivedSignature ?? "", "hex");

  return (
    expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes)
  );
}
```

Reject missing or invalid signatures with an HTTP `400` or `401` response.

## Acknowledge deliveries

Return a `2xx` response only after your application has durably accepted the
event. If processing is slow, write the verified event to your own queue and
respond immediately; the delivery request has a 10-second timeout.

Redirects and non-`2xx` responses are treated as failures. Verifycate stores at
most the first 2 KB of a failure response for troubleshooting.

## Retries

Failed deliveries are retried with this backoff schedule:

| Retry | Delay after failure |
| ----- | ------------------- |
| 1     | 30 seconds          |
| 2     | 2 minutes           |
| 3     | 15 minutes          |
| 4     | 1 hour              |
| 5     | 4 hours             |
| 6     | 12 hours            |
| 7     | 24 hours            |

After the initial attempt and seven unsuccessful retries, the delivery is
marked as permanently failed. Network errors, timeouts, and every non-`2xx`
status follow the same retry policy.

Because a response can be lost after your server processes an event, the same
event may be delivered more than once. Put a unique constraint on the payload
`id`, or record processed IDs before applying side effects.

## Event types

### Certificate lifecycle

`recipient.enrolled`, `certificate.issued`, `certificate.bulk_sent`,
`certificate.reissued`, `certificate.amended`, `certificate.deleted`,
`certificate.created`, `certificate.updated`, `certificate.archived`,
`template.published`

### Requests and corrections

`request.submitted`, `request.approved`, `request.rejected`,
`request.reopened`, `correction.submitted`, `correction.approved`,
`correction.rejected`

### Recipient engagement

`email.opened`, `email.clicked`, `certificate.opened_from_email`,
`certificate.downloaded`, `share.linkedin_add`, `share.linkedin`, `share.x`,
`share.facebook`, `share.whatsapp`, `share.telegram`, `share.email`,
`share.link_copied`, `certificate.verified`

### Forms

`form_invite.sent`, `request_form.opened`, `request_form.submitted`

Subscribe only to the events your application handles. Use `*` when forwarding
events to a general-purpose queue or data warehouse.

## Operational checklist

- Serve the endpoint over HTTPS and keep it reachable from the public internet.
- Verify signatures against the raw body before parsing JSON.
- Deduplicate by event `id` and make handlers safe to retry.
- Return `2xx` quickly; move slow work to a queue.
- Log the event ID and type, but avoid logging unnecessary recipient data.
- Monitor [delivery history](/docs/api/webhooks/deliveries) for permanent
  failures.
- Update subscriptions with [Update a webhook](/docs/api/webhooks/update) when
  your application starts or stops handling an event.

## Webhook operations

- [List configured subscriptions](/docs/api/webhooks/get)
- [Create an endpoint](/docs/api/webhooks/create)
- [Update subscriptions or status](/docs/api/webhooks/update)
- [Delete an endpoint](/docs/api/webhooks/delete)
- [List delivery attempts](/docs/api/webhooks/deliveries)
