Webhooks
Recevez les événements en temps réel et vérifiez les signatures HMAC.
À chaque événement notable (notification d'un signataire, OTP envoyé, signature acquise, demande complétée ou expirée), Signlift POSTe un payload JSON signé HMAC-SHA256 sur l'URL configurée pour votre application externe.
Configuration
L'URL de webhook est configurée par application externe dans le dashboard,
au moment où vous créez votre clé API. Le webhook_secret est affiché une
seule fois — stockez-le aux côtés de votre clé API.
Événements
Cinq types d'événements sont émis vers les webhooks aujourd'hui :
| Event | Déclencheur |
|---|---|
signer.notified | Notification d'invitation envoyée à un signataire. |
signer.otp_sent | OTP envoyé à un signataire (e-mail ou SMS). |
signer.signed | Un signataire a finalisé sa signature. |
request.completed | Tous les signataires ont signé et les PDF signés sont prêts. |
request.expired | La demande a dépassé sa fenêtre de validité. |
Headers
Chaque requête entrante porte trois headers Signlift :
| Header | Contenu |
|---|---|
X-Signlift-Event | Type d'événement (ex. request.completed). |
X-Signlift-Delivery | Identifiant unique de la tentative de livraison. Idempotence côté client. |
X-Signlift-Signature | Signature HMAC-SHA256 du body, format sha256=<hex>. |
Le Content-Type est toujours application/json.
Format du payload
{
"event": "request.completed",
"occurred_at": "2026-05-17T15:12:43Z",
"signature_request": {
"id": 42,
"status": "completed",
"mode": "sequential"
},
"signer": {
"id": 1768,
"first_name": "Jeanne",
"last_name": "Dupont",
"full_name": "Jeanne Dupont",
"email": "jeanne@example.com",
"status": "signed"
}
}event: type d'événement (identique au headerX-Signlift-Event).occurred_at: horodatage ISO 8601 UTC.signature_request: toujours présent —id,status,mode.signer: présent uniquement pour les événements liés à un signataire (signer.notified,signer.otp_sent,signer.signed).documentsetcertificate_url: présents uniquement pourrequest.completed— les URLs de téléchargement des documents signés et du certificat (voir ci-dessous).
Les URLs de téléchargement de request.completed
Le payload de request.completed contient un tableau documents (une entrée
par document, dans l'ordre de la demande) et un certificate_url unique au
niveau de la demande :
{
"event": "request.completed",
"occurred_at": "2026-05-17T15:12:43Z",
"signature_request": {
"id": 42,
"status": "completed",
"mode": "sequential"
},
"certificate_url": "https://signlift-production.s3.eu-west-3.amazonaws.com/...",
"certificate_expires_in": 900,
"documents": [
{
"document_id": 7,
"signed_url": "https://signlift-production.s3.eu-west-3.amazonaws.com/...",
"expires_in": 900
}
]
}signed_url: URL présignée du PDF signé (document + page de certificat, signature PAdES incluse).certificate_url: URL présignée du certificat de signature. Il est généré une fois par demande et couvre tous ses documents, d'où sa position à la racine du payload.expires_in/certificate_expires_in: durée de validité des URLs, en secondes. Toutes les URLs d'un même payload sont éphémères — générées à l'envoi (retries compris) et jamais stockées côté Signlift.
Les autres détails d'une demande (signataires, statuts par document, etc.)
ne sont pas inclus dans le payload — récupérez-les via
GET /api/v1/signature_requests/:id.
Vérification HMAC
Le header X-Signlift-Signature contient la signature HMAC-SHA256 du body
brut (avant tout parsing JSON), encodée en hexadécimal et préfixée par
sha256=.
require "openssl"
require "active_support/security_utils"
def verify_signlift_webhook(raw_body, header, secret)
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
ActiveSupport::SecurityUtils.secure_compare(expected, header.to_s)
endimport crypto from "node:crypto"
function verifySignliftWebhook(rawBody, header, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(header || "")
return a.length === b.length && crypto.timingSafeEqual(a, b)
}import hmac
import hashlib
def verify_signlift_webhook(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header or "")Filtrer par adresse source
En production, Signlift émet ses webhooks depuis une adresse IP fixe :
18.200.207.175Vous pouvez n'accepter que cette adresse sur votre endpoint. C'est une couche en complément de la vérification HMAC, jamais à sa place : la signature est ce qui prouve le contenu du payload, l'adresse ne dit que d'où il vient.
Le filtrage le plus efficace se fait au bord — load balancer, reverse proxy, WAF — plutôt que dans votre application : le trafic non sollicité n'atteint alors jamais votre backend, au lieu d'y être simplement rejeté.
Cette adresse est stable. Si elle devait changer, nous préviendrons les intégrations concernées au moins 30 jours à l'avance.
Retry policy
Si votre endpoint répond avec un code HTTP ≠ 2xx, est injoignable, ou met plus de 5 secondes à répondre, Signlift réessaie selon le calendrier suivant :
| Tentative | Délai depuis l'échec précédent |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 25 minutes |
| 4 | 2 heures |
| 5 | 10 heures |
Soit 5 retries au total, sur environ 13 h 30. Après 5 échecs, la livraison
est marquée failed et n'est plus retentée automatiquement.
Fiabilité : prévoyez une réconciliation
La livraison des webhooks est du meilleur effort : après épuisement des retries (endpoint indisponible ~13 h 30) ou en cas d'incident interne lors de la préparation des documents, un événement peut ne jamais vous parvenir alors que la demande a bien changé d'état.
Ne faites pas des webhooks votre unique source de vérité : si vous attendez
un request.completed qui n'arrive pas dans un délai raisonnable, interrogez
GET /api/v1/signature_requests/:id — le champ status fait foi, et la
réponse contient les download_urls régénérées. Un polling de réconciliation
(par exemple toutes les 15 minutes sur vos demandes en attente) couvre tous
les cas de perte.
Idempotence côté client
Votre handler doit être idempotent : un même événement peut être livré plusieurs fois (échec puis retry, double notification réseau, etc.).
Utilisez X-Signlift-Delivery comme clé de déduplication — chaque tentative
porte un identifiant unique, persistez-le côté votre app.
delivery_id = request.headers["X-Signlift-Delivery"]
return head :ok if WebhookEvent.exists?(delivery_id: delivery_id)
WebhookEvent.create!(delivery_id: delivery_id, ...)