Webhooks — ontvang handtekeningsgebeurtenissen in realtime
Configureer een HTTPS-URL in uw Certyneo-dashboard en ontvang een met HMAC-SHA256 ondertekende POST zodra een gebeurtenis plaatsvindt op uw enveloppen: ondertekening, weigering, vervaldatum. 11 ondersteunde gebeurtenissen, 5 leveringspogingen met exponentiële backoff, cryptografische verificatie in 8 regels code.
< 5s
Gemiddelde bezorgingstijd na de gebeurtenis
5x
Leveringspogingen in totaal, verspreid over ongeveer 1 uur 20 minuten
HMAC-SHA256
Handtekeningsalgoritme van elke aanvraag
Catalogus van gebeurtenissen
De 12 onderstaande gebeurtenissen bestrijken de volledige levenscyclus van een Certyneo-envelop. Activeer degene die u interesseren in Instellingen → Webhooks, negeer de rest — het abonnement is granulair per gebeurtenis.
| Gebeurtenis | Activering |
|---|---|
envelope.created | Een envelop wordt aangemaakt (via UI, API of template) — nuttig om een CRM-record direct na aanmaking te synchroniseren. |
envelope.sent | De envelop wordt naar de ondertekenars verzonden (eerste e-mail verzonden). Markeert het begin van de actieve ondertekeningsmyclus. |
envelope.completed | Alle ondertekenende partijen hebben ondertekend en het eIDAS-verzegelde PDF is opgeslagen. De payload bevat signedDocumentUrl, een vooraf ondertekende koppeling die 7 dagen geldig is; gebruik anders GET /v1/envelopes/id/signed-document en het audittrail via GET /v1/envelopes/id/audit-trail. |
envelope.declined | Een ondertekenende partij heeft de envelop geweigerd. Het adres van degene die weigert staat in `data.declinedBy` en de reden, indien ingevuld, in `data.reason`. |
envelope.voided | De envelop is door de afzender geannuleerd voordat de ondertekening voltooid was. Verschilt van `expired` (mens vs timeout). |
envelope.expired | De vervaldatum van de envelop is verstreken zonder volledige ondertekening. Query GET /v1/envelopes/id om de ontbrekende ondertekenende partijen te zien. |
envelope.returned_to_sender | Een ondertekenende partij heeft de envelop naar de afzender geretourneerd voor correctie, zonder deze te weigeren. De reden staat in `data.reason` en de auteur van de retournering in `data.returnedBy`. |
envelope.resubmitted | De afzender heeft een eerder geretourneerde envelop gecorrigeerd en opnieuw verzonden. Markeert de hervatting van de handtekeningencyclus. |
recipient.signed | Een individuele ondertekenende partij heeft ondertekend (maar niet noodzakelijk iedereen). Nuttig voor voortgangsmonitoring en overgang naar de volgende stap van een sequentiële werkstroom. Let op: het verzegelde PDF bestaat op dit moment nog niet, zelfs niet voor de laatste ondertekenende partij — een download die hier wordt gestart, retourneert een HTTP 409. Gebruik envelope.completed voor het document. |
recipient.viewed | Een ondertekenaar heeft de ondertekeningslink geopend zonder nog te hebben ondertekend. Nuttig voor gerichte vervolgacties. |
recipient.approved | Een fiatteur heeft de envelop goedgekeurd zonder er een handtekening op aan te brengen (intern validatiewerkstroom). Het adres staat in `data.approvedBy`. |
recipient.bounced | De e-mailserver van een ontvanger heeft de uitnodiging of herinnering definitief geweigerd (niet-bestaande postvak, dode domein). De ontvanger krijgt de status BOUNCED en automatische herinneringen stoppen. Het adres staat in `data.recipientEmail`: corrigeer het en verzend de envelop opnieuw. |
recipient.signed betekent niet "document beschikbaar"
recipient.signed wordt uitgegeven voor ELKE ondertekenende partij op het moment dat deze zijn ondertekening voltooit — inclusief de laatste, voordat het verzegelde PDF wordt samengesteld en opgeslagen. Een download die vanuit deze handler wordt geactiveerd, ontvangt dus altijd een HTTP 409 "Signed document not available until the envelope is COMPLETED". Dit is geen fout: het is een "nog niet klaar". Abonneer u op envelope.completed om het document op te halen, en gebruik recipient.signed om de voortgang bij te houden (wie heeft ondertekend en wanneer).
Payload-indeling
Alle leveringen delen hetzelfde JSON-schema op het hoogste niveau: `event`, `data` en `timestamp`. De naam van de gebeurtenis wordt ook herhaald in de header `X-Certyneo-Event`, wat routering mogelijk maakt voordat u zelfs maar de body parseert. De inhoud van `data` varieert per gebeurtenis, maar blijft altijd een plat object met eenvoudige waarden — nooit een array of genest object. Hier is een volledige `envelope.completed`-levering.
POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Delivery-Id: cm8f2h6a10004qr9k5p2wm3xt
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c{
"id": "cm8f2h6a10004qr9k5p2wm3xt",
"event": "envelope.completed",
"data": {
"envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
"sandbox": false,
"subject": "Contrat de prestation Acme Corp",
"status": "COMPLETED",
"signatureLevel": "ADVANCED",
"aesSignerCount": 2,
"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 is een vooraf ondertekende koppeling die 7 dagen geldig is vanaf de gebeurtenis: hiermee wordt een tweede geverifieerde aanroep voor het ophalen van de PDF vermeden. Dit is afwezig — en niet null — als de vooraf-ondertekening is mislukt, of op een QES-envelop die zonder opgeslagen document is voltooid; gebruik dan GET /v1/envelopes/id/signed-document, wat de waarheid blijft. Let ook op handmatige replay vanuit de dead-letter queue meer dan 7 dagen na de gebeurtenis: de koppeling in de payload is verlopen, het API-eindpunt niet.
De payload is UTF-8 gecodeerd, zonder BOM. De HMAC-handtekening wordt berekend op het onbewerkte body zoals verzonden — wijzig geen spaties, herparsen van JSON wijzigt vaak de volgorde van sleutels en verbreekt de verificatie.
HMAC-handtekening verifiëren
Elke verzoek wordt ondertekend met uw webhookgeheim (weergegeven slechts één keer bij het maken van het abonnement). De handtekening wordt doorgegeven in de header `X-Certyneo-Signature`: dit is de HMAC-SHA256 van de ruwe requestbody, gecodeerd in hexadecimaal, zonder voorvoegsel of timestamp. Verifieer ALTIJD de handtekening voordat u de payload verwerkt — zonder deze stap kan iedereen een gebeurtenis vervalsen en uw eindpunt aanroepen.
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)Veel voorkomende fout
Gebruik GEEN `===` of `==` om de verwachte handtekening met de ontvangen handtekening te vergelijken. Gebruik een timing-safe functie (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python). Zonder dit onthult het tijdsverschil in vergelijking tussen twee handtekeningen geleidelijk het geheim aan een geduldig aanvaller (timing attack).
Beleid voor opnieuw proberen
Als uw endpoint te lang duurt om te antwoorden, de verbinding weigert of een 5xx (of 429) retourneert, doen wij opnieuw een poging volgens exponentiële backoff: 5 pogingen in totaal, over ongeveer 1 u 20 min. Na de vijfde poging gaat de gebeurtenis naar de dead-letter queue — inzichtelijk en opnieuw af te spelen vanaf uw dashboard, maar niet meer automatisch opnieuw geprobeerd. Een expliciete weigering (401, 403, 404, 410, 422…) wordt echter nooit opnieuw geprobeerd: het antwoord zou niet veranderen, de gebeurtenis gaat direct naar de dead-letter queue.
| Poging | Vertraging voordat de poging | Verstreken tijd sinds de gebeurtenis |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 h 21 |
De aangegeven termijnen zijn minima: de herhalingsscan wordt ingesteld door een cron, dus een poging kan iets na het theoretische moment plaatsvinden. Verlaten gebeurtenissen worden vermeld in Webhooks → Fouten, met een handmatige afspeelknop en zonder bewaarlimieten. Een endpoint met vijf opeenvolgende definitieve fouten — of die 404/410 antwoordt — wordt automatisch uitgeschakeld en u wordt hiervan per e-mail op de hoogte gesteld: reactiveer het eenmaal hersteld, de teller begint opnieuw (elke succesvolle bezorging stelt het ook op nul).
Testen zonder een echte envelop te verzenden
Maak eerst een abonnement — het antwoord bevat het `secret` dat nodig is voor HMAC-verificatie, slechts eenmaal weergegeven. Vanuit Instellingen → Webhooks stuurt de knop "Testen" vervolgens een ondertekend POST precies zoals een echte levering en toont u het ruwe antwoord van uw server: dit is de snelste manier om uw handler te valideren, lokaal via ngrok of in continue integratie.
# 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"]
}'De URL moet openbaar bereikbaar zijn via HTTPS: een privé-adres of localhost wordt bij abonnementcreatie afgewezen door SSRF-bescherming.
Abonnementen zijn per omgeving gescheiden, net als de sleutels: een webhook die met een sk_test_-sleutel is aangemaakt, ontvangt alleen de gebeurtenissen van testenveloppen, een webhook met een sk_live_-sleutel alleen die van echte enveloppen. Een testenvelop bereikt uw productie-URL dus nooit, en elke payload vermeldt „sandbox” (true of false).
6 praktijken om na te leven
- Controleer de HMAC-handtekening VOOR elke body-leezing — gebruik een timing-safe vergelijking.
- Dedupliceren op de header `X-Certyneo-Delivery-Id` (identiek aan `id` in het hoofdgedeelte) door reeds geziene identifiers in de database op te slaan — een replay kan een reeds verwerkte gebeurtenis opnieuw bezorgen als uw 2xx verloren is gegaan, met dezelfde identifier bij elke poging.
- HTTP 2xx antwoord binnen maximaal 10 seconden geven, daarna asynchroon verwerken (queue). Daarna wordt de levering beëindigd en als fout geteld.
- Log de onbewerkte body + volledige handtekening in debug — HMAC-verificatie mislukt vaak vanwege een BOM of onzichtbare witruimte.
- Controleer de pagina Webhooks → Fouten: u wordt per e-mail op de hoogte gesteld als uw endpoint is uitgeschakeld, maar niet per gebeurtenis — als een gebeurtenis zijn pogingen uitput, dient u het zelf opnieuw af te spelen.
- Filter gebeurtenissen bij aanmelding in plaats van in uw handler, en blijf onder de limiet van 5 abonnementen per account.
U hoeft geen eindpunt te hosten om deze gebeurtenissen te ontvangen: de connector stelt het abonnement voor u in en start de stroom rechtstreeks bij een envelopegebeurtenis. Zie Certyneo voor Power Automate en Microsoft 365.
Meer informatie
Klaar om uw systemen aan te sluiten?
Webhooks en REST API zijn vanaf het Standard-plan inbegrepen. Maak uw account aan, genereer een `sk_test_`-sleutel en verbind uw endpoint in sandbox voordat u naar productie gaat.