Вебхуки
Bona Fide отправляет HTTPS-запрос POST на вашу конечную точку для каждого события конверта, на которое вы подписаны. Доставки соответствуют
спецификации Standard Webhooks, поэтому проверить их может любая из её библиотек.
Регистрация конечной точки
Заголовок раздела «Регистрация конечной точки»В разделе Настройки › Вебхуки или с помощью ключа API с областью доступа org:manage:
curl https://api.bonafidesign.com/v1/webhook-endpoints \ -H "Authorization: Bearer $BONA_FIDE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/hooks/bona-fide", "description": "Production", "events": ["envelope.completed", "envelope.sealed", "envelope.declined"] }'Ответ содержит secret (whsec_…) — только в этот раз. Пустой список events подписывает на все события.
URL должен быть публичным адресом https://. Конечная точка относится к одному режиму: конечные точки, созданные тестовым ключом, получают только
события конвертов в тестовом режиме, а рабочие — только рабочие события.
PATCH /v1/webhook-endpoints/{id} меняет URL, события или описание, а также приостанавливает или возобновляет работу конечной точки
(enabled). POST /v1/webhook-endpoints/{id}/test отправляет тестовый запрос webhook.test.
События
Заголовок раздела «События»| Тип | Когда |
|---|---|
envelope.created |
Конверт создан |
envelope.ready |
Документ сформирован, и конверт готов для получателей |
recipient.sent |
Наступила очередь получателя (а при доставке по почте ему отправлена ссылка) |
recipient.viewed |
Получатель впервые открыл страницу подписания |
recipient.signed |
Получатель отправил подпись (или контрподпись была поставлена автоматически) |
recipient.declined |
Получатель отказался |
envelope.completed |
Все получатели завершили работу. data.values содержит переменные и все введённые значения |
envelope.sealed |
Запечатанный PDF готов к скачиванию |
envelope.declined |
Конверт завершён, потому что получатель отказался |
envelope.voided |
Вы аннулировали конверт |
envelope.expired |
expiresAt наступило до завершения |
signing_link.used |
Посетитель создал конверт по постоянной ссылке для подписания |
acceptance.created зарезервировано в схеме и пока не генерируется.
Полезная нагрузка
Заголовок раздела «Полезная нагрузка»{ "id": "evt_9sKq2Lm4Tz", "type": "envelope.completed", "createdAt": "2026-10-13T14:09:52.004Z", "testMode": false, "data": { "envelope": { "id": "env_8Gm2kQ4rT1", "status": "completed", "seal": "pending", "recipients": [{ "id": "rcp_5Jw0pLs9Xe", "role": "client", "status": "completed", "…": "…" }], "externalId": "user_8841", "metadata": {}, "…": "…" }, "values": { "variables": { "client": { "name": "Acme Robotics, Inc." }, "effective_date": "2026-10-13" }, "recipients": { "client": { "client_title": "CEO", "start_date": "2026-11-01" } } } }}data.envelope — полный конверт на момент события. data.values есть только в
envelope.completed: отправленные вами переменные и значения полей каждой роли по идентификатору поля.
Заголовки и подпись
Заголовок раздела «Заголовки и подпись»Каждая доставка содержит:
| Заголовок | Значение |
|---|---|
webhook-id |
Идентификатор события (evt_…); одинаков при каждом повторе. Используйте его для дедупликации |
webhook-timestamp |
Время подписания этой попытки в секундах Unix |
webhook-signature |
v1,<base64 HMAC-SHA256>, через пробел, если подписывает несколько секретов |
user-agent |
BonaFide-Webhooks/1 |
Подпись — это HMAC-SHA256 от {webhook-id}.{webhook-timestamp}.{raw body} с ключом, полученным декодированием из base64 части
секрета после whsec_. Проверяйте необработанное тело (до разбора JSON), сравнивайте за постоянное время и отклоняйте
метки времени, отличающиеся от ваших часов более чем на пять минут.
import express from 'express';import { Webhook } from 'standardwebhooks'; // npm i standardwebhooks
const wh = new Webhook(process.env.BONA_FIDE_WEBHOOK_SECRET!); // whsec_…const app = express();
app.post('/hooks/bona-fide', express.raw({ type: 'application/json' }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers as Record<string, string>); // throws if forged or too old } catch { return res.sendStatus(400); } queue.add(event); // do the work asynchronously and answer quickly res.sendStatus(204);});# pip install standardwebhooks flaskimport osfrom flask import Flask, requestfrom standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["BONA_FIDE_WEBHOOK_SECRET"]) # whsec_…app = Flask(__name__)
@app.post("/hooks/bona-fide")def bona_fide(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 400 enqueue(event) return "", 204import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret: string, headers: Record<string, string>, rawBody: string) { const id = headers['webhook-id']; const ts = Number(headers['webhook-timestamp']); if (!id || !ts || Math.abs(Date.now() / 1000 - ts) > 300) throw new Error('stale or missing headers'); const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest(); const ok = (headers['webhook-signature'] ?? '').split(' ').some((part) => { const [version, sig] = part.split(','); const given = Buffer.from(sig ?? '', 'base64'); return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected); }); if (!ok) throw new Error('bad signature'); return JSON.parse(rawBody);}Повторы
Заголовок раздела «Повторы»Доставка считается успешной при любом ответе 2xx в течение 10 секунд. Перенаправления не выполняются. Во всех остальных случаях доставка повторяется
с увеличивающимися интервалами: через 5 секунд, 5 минут, 30 минут, 2 часа, 5 часов, 10 часов, 14 часов, 20 часов и 24 часа,
но не дольше трёх дней после события. После этого доставка помечается как неудачная.
- Отвечайте быстро и обрабатывайте асинхронно; выполняйте дедупликацию по
webhook-id. - Доставки могут приходить не по порядку: проверяйте
data.envelope.status, а не рассчитывайте на определённую последовательность. GET /v1/webhook-endpoints/{id}/deliveriesвыводит последние попытки с кодами статуса, ошибками и временем, а для каждой конечной точки показывается доля успешных доставок за 24 часа (health).POST /v1/events/{id}/redeliverповторно отправляет событие на конечную точку, например после сбоя.
Ротация секрета
Заголовок раздела «Ротация секрета»POST /v1/webhook-endpoints/{id}/secret/rotate сразу возвращает новый секрет. В течение следующих 24 часов каждая
доставка подписывается и новым, и старым секретом (две записи в webhook-signature), поэтому вы можете
развернуть новый секрет, не теряя событий.