Webhooks
Des avis signés envoyés à votre serveur quand des documents changent.
Ajouter un point de terminaison
Ajoutez un point de terminaison HTTPS dans Paramètres, Développeurs, ou avec POST /webhooks. Choisissez les événements voulus, ou * pour tous. La réponse contient le secret de signature (whsec_…); vous pourrez le revoir plus tard dans l'application ou avec GET /webhooks/{id}.
curl -X POST "https://api.esorasign.com/v1/webhooks" \
-H "Authorization: Bearer $ESORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/esora/webhooks", "events": ["document.completed", "recipient.signed"] }'Les points de terminaison doivent être des adresses HTTPS publiques. Les adresses de réseaux privés et locaux sont refusées.
Ce que nous envoyons
Chaque livraison est un POST avec un événement JSON et ces en-têtes :
- Esora-Signature: t=1762162887,v1=5f1c… (voir plus bas)
- Esora-Event-Id : l'identifiant de l'événement (evt_…). Servez-vous-en pour ignorer les doublons.
- Esora-Delivery-Id : l'identifiant de cette tentative de livraison (dlv_…).
- User-Agent: eSora-Sign-Webhooks/1.0
data.object est le document complet, tel que GET /documents/{id} le renvoie. Les événements de destinataire contiennent aussi data.recipient_id.
{
"id": "evt_5ydfhl3TvtnythpZPPPP6U",
"object": "event",
"type": "recipient.signed",
"created_at": "2026-11-03T09:41:27.000Z",
"data": {
"object": {
"id": "doc_3y9Pf34qY6Nb3wWD25RQ4F",
"object": "document",
"status": "sent",
"title": "Service agreement - Northwind",
"recipients": [ { "id": "rcp_5ZR3qa7yEeeby3abP3E2Zs", "object": "recipient", "status": "signed", "...": "..." } ],
"metadata": { "crm_deal_id": "D-10442" },
"...": "the full document object"
},
"recipient_id": "rcp_5ZR3qa7yEeeby3abP3E2Zs"
}
}Vérifier la signature
v1 est un HMAC-SHA256 de l'horodatage, d'un point et du corps brut de la requête, avec votre secret de signature. Calculez-le vous-même et comparez en temps constant. Rejetez les horodatages trop vieux (5 minutes est une bonne limite) pour bloquer les rejeux. Utilisez le corps brut : analyser puis resérialiser le JSON le modifie.
// Node 18 or later with Express (npm install express).
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.ESORA_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;
function verify(rawBody, header) {
const parts = header.split(',').map((p) => p.split('='));
const t = Number(parts.find(([k]) => k === 't')?.[1]);
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!t || Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');
return sigs.some((s) => s.length === expected.length && timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
const app = express();
app.post('/esora/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body.toString('utf8'), req.get('Esora-Signature') ?? '')) return res.sendStatus(400);
const event = JSON.parse(req.body);
// Deliveries can repeat: skip event.id values you have already handled.
if (event.type === 'document.completed') {
console.log('completed', event.data.object.id, event.data.object.metadata);
}
res.sendStatus(200); // answer within 10 seconds; do slow work afterwards
});
app.listen(3000);
# Python 3.9 or later with Flask (pip install flask).
import hashlib, hmac, os, time
from flask import Flask, request, abort
SECRET = os.environ["ESORA_WEBHOOK_SECRET"].encode() # whsec_...
app = Flask(__name__)
def verify(raw_body: bytes, header: str) -> bool:
items = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in items if k == "t"), None)
sigs = [v for k, v in items if k == "v1"]
if not t or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(s, expected) for s in sigs)
@app.post("/esora/webhooks")
def esora_webhooks():
if not verify(request.get_data(), request.headers.get("Esora-Signature", "")):
abort(400)
event = request.get_json()
if event["type"] == "document.completed":
print("completed", event["data"]["object"]["id"])
return "", 200
Nouvelles tentatives et rotation
- Répondez avec un statut 2xx en moins de 10 secondes. Les redirections ne sont pas suivies et comptent comme des échecs.
- Les livraisons échouées sont réessayées jusqu'à 16 fois, avec une attente de plus en plus longue (de 10 secondes jusqu'à une heure), pendant environ 8 heures en tout. La livraison est ensuite marquée failed.
- Consultez les livraisons récentes dans l'application ou avec GET /webhooks/{id}/deliveries, et renvoyez-en une avec POST /webhooks/{id}/deliveries/{delivery_id}/retry.
- POST /webhooks/{id}/test envoie tout de suite un événement webhook.test et vous dit comment votre point de terminaison a répondu.
- Faites la rotation du secret avec POST /webhooks/{id}/rotate-secret. Pendant 24 heures, nous signons avec le nouveau et l'ancien secret (deux valeurs v1), pour que vous puissiez changer sans rien manquer.
Les événements sont aussi conservés 90 jours à GET /events, pour rattraper le retard après une panne.