网络网络即时收到签名活动
Configurez une URL HTTPS dans votre dashboard Certyneo et recevez un POST signé HMAC-SHA256 dès qu'un événement se produit sur vos enveloppes : signature, refus, expiration. 11 événements supportés, 5 tentatives de livraison en backoff exponentiel, vérification cryptographique en 8 lignes de code.
< 5s
事件后的平均交付时间
5x
Tentatives de livraison au total, étalées sur environ 1 h 20
HMAC-SHA256
每个请求的签名算法
活动目录
Les 11 événements ci-dessous couvrent l'intégralité du cycle de vie d'une enveloppe Certyneo. Activez ceux qui vous intéressent dans Paramètres → Webhooks, ignorez les autres — l'abonnement est granulaire par événement.
| 活动 | 发作 |
|---|---|
envelope.created | 创建一个包裹 (通过UI,API或模板) 很有用,可以在创建时同步CRM侧记录. |
envelope.sent | 封信发送给签名者 (首次发送电子邮件).标志着活跃签名循环的开始. |
envelope.completed | Tous les signataires ont signé et le PDF scellé eIDAS est stocké. Le payload porte signedDocumentUrl, un lien pré-signé valable 7 jours ; à défaut, GET /v1/envelopes/id/signed-document, et la piste d'audit via GET /v1/envelopes/id/audit-trail. |
envelope.declined | Un signataire a refusé l'enveloppe. L'adresse du refusant est dans `data.declinedBy` et la raison, si elle a été saisie, dans `data.reason`. |
envelope.voided | 封面被发行人取消了,直到签名完成. |
envelope.expired | La date d'expiration de l'enveloppe est passée sans signature complète. Interrogez GET /v1/envelopes/id pour connaître les signataires manquants. |
envelope.returned_to_sender | Un signataire a renvoyé l'enveloppe à l'émetteur pour correction, sans la refuser. La raison est dans `data.reason` et l'auteur du renvoi dans `data.returnedBy`. |
envelope.resubmitted | L'émetteur a corrigé puis renvoyé une enveloppe précédemment retournée. Marque la reprise du cycle de signature. |
recipient.signed | Un signataire individuel a signé (mais pas forcément tous). Utile pour suivre l'avancement et enchaîner sur l'étape suivante d'un workflow séquentiel. Attention : le PDF scellé n'existe pas encore à ce stade, même pour le dernier signataire — un téléchargement lancé ici renvoie un HTTP 409. Passez par envelope.completed pour le document. |
recipient.viewed | 一位签名者在未签署之前打开了签名链接,这对于有针对性的商业启动很有用. |
recipient.approved | Un approbateur a validé l'enveloppe sans y apposer de signature (workflow de validation interne). L'adresse est dans `data.approvedBy`. |
recipient.signed ne signifie pas « document disponible »
recipient.signed est émis pour CHAQUE signataire, au moment où il termine sa signature — y compris le dernier, avant que le PDF scellé ne soit assemblé et stocké. Un téléchargement déclenché depuis ce handler reçoit donc toujours un HTTP 409 « Signed document not available until the envelope is COMPLETED ». Ce n'est pas une erreur : c'est un « pas encore prêt ». Abonnez-vous à envelope.completed pour récupérer le document, et gardez recipient.signed pour suivre l'avancement (qui a signé, et quand).
有效载荷的格式
Toutes les livraisons partagent le même schéma JSON top-level : `event`, `data` et `timestamp`. Le nom de l'événement est aussi répété dans l'en-tête `X-Certyneo-Event`, ce qui permet de router avant même de parser le corps. Le contenu de `data` varie selon l'événement, mais reste toujours un objet plat de valeurs simples — jamais de tableau ni d'objet imbriqué. Voici une livraison `envelope.completed` complète.
POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c{
"event": "envelope.completed",
"data": {
"envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
"subject": "Contrat de prestation Acme Corp",
"status": "COMPLETED",
"completedAt": "2026-05-27T08:42:13.000Z",
"recipientCount": 2,
"recipients": [
{ "email": "alice@acme.com", "name": "Alice Martin", "role": "SIGNER", "status": "SIGNED" },
{ "email": "bob@acme.com", "name": "Bob Durand", "role": "SIGNER", "status": "SIGNED" }
],
"signedDocumentUrl": "https://storage.certyneo.com/signed/cm7x2k9p4.../contrat.pdf?X-Amz-Expires=604800&X-Amz-Signature=..."
},
"timestamp": "2026-05-27T08:42:13.521Z"
}signedDocumentUrl est un lien pré-signé valable 7 jours à compter de l'événement : il évite un second appel authentifié pour récupérer le PDF. Il est absent — et non pas null — si la pré-signature a échoué, ou sur une enveloppe QES complétée sans document stocké ; repassez alors par GET /v1/envelopes/id/signed-document, qui reste la source de vérité. Attention aussi au rejeu manuel depuis la dead-letter queue plus de 7 jours après l'événement : le lien du payload est expiré, l'endpoint API non.
收件符号是UTF-8编码,没有BOM.HMAC签名计算在原始体内,如发送不要改变空格,JSON重新解析经常改变密钥的顺序,破坏验证.
检查HMAC的签名
Chaque requête est signée avec votre secret webhook (affiché une seule fois à la création de l'abonnement). La signature est transmise dans l'en-tête `X-Certyneo-Signature` : c'est le HMAC-SHA256 du corps brut de la requête, encodé en hexadécimal, sans préfixe ni horodatage. Vérifiez TOUJOURS la signature avant de traiter le payload — sans cette étape, n'importe qui peut forger un événement et appeler votre endpoint.
Node.js / TypeScript
import crypto from "node:crypto";
export function verifyCertyneoSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// X-Certyneo-Signature = HMAC-SHA256(secret, rawBody), hex-encoded.
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = Buffer.from(signatureHeader, "hex");
const computed = Buffer.from(expected, "hex");
// timingSafeEqual throws when the two buffers differ in length —
// a malformed header must return false, not crash the handler.
if (received.length !== computed.length) return false;
// timingSafeEqual to mitigate timing attacks.
return crypto.timingSafeEqual(computed, received);
}Python
import hashlib
import hmac
def verify_certyneo_signature(
raw_body: bytes, signature_header: str, secret: str
) -> bool:
"""Verify a Certyneo webhook signature.
`X-Certyneo-Signature` holds the hex-encoded HMAC-SHA256 of the
raw request body, computed with your webhook secret.
"""
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)常见的错误
不要使用 `===`或 `==`来比较预期和接收的签名.使用一个时间安全函数 (`crypto.timingSafeEqual`在Node中, `hmac.compare_digest`在Python中).否则,两个签名之间的比较时间差距会逐渐向耐心的攻击者揭示秘密 (时间攻击).
试用政策
Si votre endpoint met trop de temps à répondre, refuse la connexion ou renvoie un 5xx (ou un 429), nous retentons selon un backoff exponentiel : 5 tentatives au total, sur environ 1 h 20. Passé la cinquième, l'événement part en dead-letter queue — consultable et rejouable depuis votre dashboard, mais plus retenté automatiquement. Un refus explicite (401, 403, 404, 410, 422…) n'est en revanche jamais retenté : la réponse ne changerait pas, l'événement part directement en dead-letter queue.
| 试图 | Délai avant la tentative | Temps écoulé depuis l'événement |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 h 21 |
Les délais indiqués sont des minimums : le balayage de retry est cadencé par un cron, une tentative peut donc partir légèrement après l'heure théorique. Les événements abandonnés sont listés dans Webhooks → Échecs, avec un bouton de rejeu manuel et sans limite de durée de conservation. Un endpoint qui enchaîne cinq échecs définitifs — ou qui répond 404 / 410 — est désactivé automatiquement, et vous en êtes prévenu par email : réactivez-le une fois corrigé, le compteur repart de zéro (toute livraison réussie le remet également à zéro).
试验没有发送真正的信封
Créez d'abord un abonnement — la réponse contient le `secret` nécessaire à la vérification HMAC, affiché une seule fois. Depuis Paramètres → Webhooks, le bouton « Tester » envoie ensuite un POST signé exactement comme une vraie livraison et vous affiche la réponse brute de votre serveur : c'est le moyen le plus rapide de valider votre handler, en local via ngrok ou en intégration continue.
# Create a subscription — "secret" is returned once, store it now.
curl -X POST https://api.certyneo.com/v1/webhooks \
-H "Authorization: Bearer $CERTYNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your.app/webhooks/certyneo",
"events": ["envelope.completed", "recipient.signed"]
}'L'URL doit être publiquement joignable en HTTPS : une adresse privée ou localhost est rejetée par la protection SSRF au moment de la création de l'abonnement.
六个需要遵守的做法
- 检查HMAC签名在读取任何体内之前 使用时间安全的比较.
- Traiter chaque `data.envelopeId` × `event` une seule fois en stockant les couples déjà vus en base — un retry peut relivrer un événement que vous avez déjà traité si votre 2xx s'est perdu.
- Répondre HTTP 2xx sous 10 secondes maximum, puis traiter en asynchrone (queue). Au-delà, la livraison est coupée et comptée comme un échec.
- ,HMAC检查通常会失败,因为BOM或空白空间是看不见的.
- Surveiller la page Webhooks → Échecs : vous êtes prévenu par email si votre endpoint est désactivé, mais pas événement par événement — un événement qui épuise ses tentatives, c'est à vous d'aller le rejouer.
- Filtrer les événements à la souscription plutôt que dans votre handler, et rester sous la limite de 5 abonnements par compte.
Vous n'êtes pas obligé d'héberger un point de terminaison pour recevoir ces évènements : le connecteur pose l'abonnement pour vous et démarre le flux directement sur un évènement d'enveloppe. Voir Certyneo pour Power Automate et Microsoft 365.
为了更进一步
准备好连接系统吗?
Les webhooks et l'API REST sont inclus à partir du plan Standard. Créez votre compte, générez une clé `sk_test_` et branchez votre endpoint en sandbox avant de passer en production.