Naar hoofdinhoud gaan
Certyneo
Ontwikkelaarsdocumentatie

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.

GebeurtenisActivering
envelope.createdEen envelop wordt aangemaakt (via UI, API of template) — nuttig om een CRM-record direct na aanmaking te synchroniseren.
envelope.sentDe envelop wordt naar de ondertekenars verzonden (eerste e-mail verzonden). Markeert het begin van de actieve ondertekeningsmyclus.
envelope.completedAlle 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.declinedEen 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.voidedDe envelop is door de afzender geannuleerd voordat de ondertekening voltooid was. Verschilt van `expired` (mens vs timeout).
envelope.expiredDe vervaldatum van de envelop is verstreken zonder volledige ondertekening. Query GET /v1/envelopes/id om de ontbrekende ondertekenende partijen te zien.
envelope.returned_to_senderEen 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.resubmittedDe afzender heeft een eerder geretourneerde envelop gecorrigeerd en opnieuw verzonden. Markeert de hervatting van de handtekeningencyclus.
recipient.signedEen 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.viewedEen ondertekenaar heeft de ondertekeningslink geopend zonder nog te hebben ondertekend. Nuttig voor gerichte vervolgacties.
recipient.approvedEen fiatteur heeft de envelop goedgekeurd zonder er een handtekening op aan te brengen (intern validatiewerkstroom). Het adres staat in `data.approvedBy`.
recipient.bouncedDe 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.

PogingVertraging voordat de pogingVerstreken tijd sinds de gebeurtenis
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 15 min21 min
#5+ 1 h1 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.