Веб-хоки получать подписи событий в режиме реального времени
Настройте HTTPS URL в своей панели управления Certyneo и получите подписанную HMAC-SHA256 POST, как только на ваших конвертах произойдет событие: подпись, отказ, истечение срока действия. 8 поддерживаемых событий, экспоненциальная повторная пробная работа на 6 попыток, криптографическая проверка в 8 строках кода.
< 5s
Среднее время доставки после события
5x
Попытки при неудачах (до ~ 9 часов позже)
HMAC-SHA256
Алгоритм подписи каждого запроса
Каталог событий
8 событий, приведенных ниже, охватывают весь жизненный цикл конверта Certyneo.
| Событие | Вспышка |
|---|---|
envelope.created | Создается конверт (по UI, API или шаблону) полезный для синхронизации записи на стороне CRM с момента создания. |
envelope.sent | Заявление отправляется подписантам (первое отправленное электронное письмо). |
envelope.completed | Все подписавшиеся подписали. eIDAS и аудит траектории доступны через `proof_pdf_url` в полезной нагрузке. |
envelope.declined | Причина отказа (если она указана подписантом) указана в `data.decline_reason`. |
envelope.voided | Конверт был аннулирован эмитентом до полной подписи. |
envelope.expired | Дата истечения срока действия конверта прошла без полной подписи. Список отсутствующих подписантов находится в `data.missing_signers`. |
recipient.signed | Полезно для последовательных рабочих процессов: запускать переход к следующему подписавшему. |
recipient.viewed | Подписчик открыл ссылку, не подписав ее, что полезно для целенаправленных торгово-развлекательных мероприятий. |
Формат полезной нагрузки
Все события имеют одну и ту же схему JSON верхнего уровня: `event`, `envelope_id`, `occurred_at`, `data`. Содержание `data` варьируется в зависимости от события (поле, документированные по событиям в API-ссылке).
{
"event": "envelope.completed",
"envelope_id": "env_01HG3K8X9Y2N4P5Q6R7S8T9V0W",
"occurred_at": "2026-05-27T08:42:13.521Z",
"data": {
"title": "Contrat de prestation Acme Corp",
"status": "completed",
"created_at": "2026-05-24T14:12:00.000Z",
"completed_at": "2026-05-27T08:42:13.000Z",
"signers": [
{
"email": "client@acme.example",
"name": "Alex Client",
"signed_at": "2026-05-27T08:42:13.000Z",
"method": "eidas_aes",
"ip": "203.0.113.42"
}
],
"proof_pdf_url": "https://certyneo.com/api/envelopes/env_01HG.../proof.pdf"
}
}Подпись HMAC вычисляется на грубом теле, как отправлено не изменяйте пробелы, перепроверка JSON часто изменяет порядок ключей и нарушает проверку.
Проверьте подпись HMAC
Каждый запрос подписывается вашим секретом вебхука (видим только один раз при создании в панели управления, затем шифрован в покое).<timestamp>,v1=<hex_hmac>Всегда проверяйте подпись перед обработкой полезной нагрузки без этого шага любой может создать событие и вызвать ваш конечный пункт.
Node.js / TypeScript
import crypto from "node:crypto";
export function verifyCertyneoSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// Header format: t=<timestamp>,v1=<hex_hmac>
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=")),
);
const ts = parts.t;
const sig = parts.v1;
if (!ts || !sig) return false;
// Reject events older than 5 minutes (replay protection).
const age = Math.abs(Date.now() / 1000 - Number(ts));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
// timingSafeEqual to mitigate timing attacks.
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(sig, "hex"),
);
}Python
import hashlib
import hmac
import time
def verify_certyneo_signature(
raw_body: bytes, signature_header: str, secret: str
) -> bool:
"""Verify a Certyneo webhook signature.
Header format: `t=<timestamp>,v1=<hex_hmac>`
Rejects events older than 5 minutes (replay protection).
"""
parts = dict(p.split("=") for p in signature_header.split(","))
ts = parts.get("t")
sig = parts.get("v1")
if not ts or not sig:
return False
if abs(time.time() - int(ts)) > 300:
return False
expected = hmac.new(
secret.encode("utf-8"),
f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sig)Часто встречающиеся ошибки
Не используйте `===` или `==` для сравнения ожидаемой подписи с полученной. Используйте функцию timing-safe (`crypto.timingSafeEqual` в Node, `hmac.compare_digest` в Python). Иначе разрыв времени сравнения между двумя подписями постепенно раскрывает секрет пациентскому атакующему (timing attack).
Политика повторного просмотра
Если ваша конечная точка отвечает не на HTTP 2xx (timeout, 5xx, connection refused), мы откликаемся по экспоненциальному backoff. Всего 6 попыток за ~9 часов. После 6 попыток событие помечается как `failed` в вашей панели управления веб-хока и отправляется предупредительное электронное письмо.
| Попытка | Сборный срок | Прошлое время |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 30 min | 36 min |
| #5 | + 2 h | 2h 36 |
| #6 | + 6 h | 8h 36 |
Если вы хотите вручную пересылать неудавшееся событие, панель управления веб-хока предлагает кнопку Перезагрузка на каждой неудавшейся доставке доступная в течение 7 дней после окончательного отказа.
Проверка без отправки настоящего конверта
Конечная точка `/v1/webhooks/test` принимает URL и тип события, а также ПОСТИрует вымышленную полезную нагрузку, подписанную точно как настоящая.
# Trigger a test webhook to your endpoint
curl -X POST https://api.certyneo.com/v1/webhooks/test \
-H "Authorization: Bearer $CERTYNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event": "envelope.completed", "url": "https://your.app/webhooks/certyneo"}'6 практических советов
- Проверьте подпись HMAC ПРЕД любым чтением тела используйте сравнение с безопасным временем.
- Обрабатывать каждый `envelope_id` × `event` только один раз, храня ID, которые уже были просмотрены в базе (взрывы могут повторно выводить одно и то же событие).
- Ответьте на HTTP 200 не более чем за 5 секунд, а затем обрабатывайте в ассинхронном режиме (цепочка).
- Регистрация грубого тела + полная подпись в дебгу проверка HMAC часто терпит неудачу на BOM или невидимом белом пространстве.
- Подключить dead-letter на `failed` события, чтобы пересылать в случае инцидента вручную на стороне вашего отдела.
- Позволить отставание 5 минут между `t=` и временем приема и далее, отклонить, чтобы заблокировать ретрансляции.
Чтобы пойти дальше
Готовы подключить свои системы?
Создайте бесплатную учетную запись за 2 минуты настройка webhook доступна с самого стартер-плана (бесплатно, 5 конвертов/месяц).