Webhooks
Bona Fide envía un POST HTTPS a su endpoint por cada evento de sobre al que se suscriba. Las entregas siguen la
especificación Standard Webhooks, de modo que cualquiera de sus bibliotecas
puede verificarlas.
Registro de un endpoint
Sección titulada «Registro de un endpoint»En Configuración › Webhooks, o con una clave de API que tenga el ámbito
org:manage:
curl https://api.bonafidesign.com/v1/webhook-endpoints \ -H "Authorization: Bearer $BONA_FIDE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/hooks/bona-fide", "description": "Production", "events": ["envelope.completed", "envelope.sealed", "envelope.declined"] }'La respuesta incluye secret (whsec_…), solo esta vez. Una lista events vacía suscribe el endpoint a todos
los eventos. La URL debe ser pública y empezar por https://. Cada endpoint pertenece a un único modo: los
endpoints creados con una clave de prueba solo reciben eventos de sobres en modo de prueba, y los de producción,
solo eventos de producción.
PATCH /v1/webhook-endpoints/{id} cambia la URL, los eventos o la descripción, y pausa o reanuda el endpoint
(enabled). POST /v1/webhook-endpoints/{id}/test envía un ping webhook.test.
Eventos
Sección titulada «Eventos»| Tipo | Cuándo |
|---|---|
envelope.created |
Se ha creado el sobre |
envelope.ready |
El documento se ha generado y el sobre está listo para sus destinatarios |
recipient.sent |
Le ha llegado el turno a un destinatario (y, con la entrega por correo, se le ha enviado su enlace) |
recipient.viewed |
Un destinatario ha abierto la página de firma por primera vez |
recipient.signed |
Un destinatario ha enviado su firma (o se ha contrafirmado automáticamente) |
recipient.declined |
Un destinatario ha rechazado la firma |
envelope.completed |
Todos los destinatarios han terminado. data.values contiene las variables y todos los valores introducidos |
envelope.sealed |
El PDF sellado está listo para descargarse |
envelope.declined |
El sobre ha terminado porque un destinatario ha rechazado la firma |
envelope.voided |
Usted ha anulado el sobre |
envelope.expired |
expiresAt ha pasado antes de completarse |
signing_link.used |
Un visitante ha iniciado un sobre desde un enlace de firma permanente |
acceptance.created está reservado en el esquema y aún no se emite.
{ "id": "evt_9sKq2Lm4Tz", "type": "envelope.completed", "createdAt": "2026-10-13T14:09:52.004Z", "testMode": false, "data": { "envelope": { "id": "env_8Gm2kQ4rT1", "status": "completed", "seal": "pending", "recipients": [{ "id": "rcp_5Jw0pLs9Xe", "role": "client", "status": "completed", "…": "…" }], "externalId": "user_8841", "metadata": {}, "…": "…" }, "values": { "variables": { "client": { "name": "Acme Robotics, Inc." }, "effective_date": "2026-10-13" }, "recipients": { "client": { "client_title": "CEO", "start_date": "2026-11-01" } } } }}data.envelope es el sobre completo en el momento del evento. data.values solo aparece en
envelope.completed: las variables que ha enviado y los valores de los campos de cada rol, por ID de campo.
Cabeceras y firma
Sección titulada «Cabeceras y firma»Cada entrega incluye:
| Cabecera | Valor |
|---|---|
webhook-id |
El ID del evento (evt_…); es el mismo en todos los reintentos. Utilícelo para eliminar duplicados |
webhook-timestamp |
Segundos Unix del momento en que se firmó este intento |
webhook-signature |
v1,<base64 HMAC-SHA256>, separados por espacios cuando firma más de un secreto |
user-agent |
BonaFide-Webhooks/1 |
La firma es un HMAC-SHA256 sobre {webhook-id}.{webhook-timestamp}.{raw body}, cuya clave es la parte del secreto
que sigue a whsec_, decodificada en base64. Verifique el cuerpo en bruto (antes de analizar el JSON), compare
en tiempo constante y rechace las marcas de tiempo que se alejen más de cinco minutos de su reloj.
import express from 'express';import { Webhook } from 'standardwebhooks'; // npm i standardwebhooks
const wh = new Webhook(process.env.BONA_FIDE_WEBHOOK_SECRET!); // whsec_…const app = express();
app.post('/hooks/bona-fide', express.raw({ type: 'application/json' }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers as Record<string, string>); // throws if forged or too old } catch { return res.sendStatus(400); } queue.add(event); // do the work asynchronously and answer quickly res.sendStatus(204);});# pip install standardwebhooks flaskimport osfrom flask import Flask, requestfrom standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["BONA_FIDE_WEBHOOK_SECRET"]) # whsec_…app = Flask(__name__)
@app.post("/hooks/bona-fide")def bona_fide(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 400 enqueue(event) return "", 204import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret: string, headers: Record<string, string>, rawBody: string) { const id = headers['webhook-id']; const ts = Number(headers['webhook-timestamp']); if (!id || !ts || Math.abs(Date.now() / 1000 - ts) > 300) throw new Error('stale or missing headers'); const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest(); const ok = (headers['webhook-signature'] ?? '').split(' ').some((part) => { const [version, sig] = part.split(','); const given = Buffer.from(sig ?? '', 'base64'); return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected); }); if (!ok) throw new Error('bad signature'); return JSON.parse(rawBody);}Reintentos
Sección titulada «Reintentos»Una entrega se considera correcta con cualquier 2xx en un plazo de 10 segundos. Las redirecciones no se siguen.
Todo lo demás se reintenta con esperas crecientes, a los 5 segundos, 5 minutos, 30 minutos, 2 horas, 5 horas,
10 horas, 14 horas, 20 horas y 24 horas, durante tres días como máximo desde el evento. Después, la entrega se
marca como fallida.
- Responda rápidamente y procese de forma asíncrona; elimine duplicados por
webhook-id. - Las entregas pueden llegar desordenadas: compruebe
data.envelope.statusen lugar de dar por hecho un orden. GET /v1/webhook-endpoints/{id}/deliveriesmuestra los intentos recientes con códigos de estado, errores y tiempos, y cada endpoint indica su tasa de éxito de las últimas 24 horas (health).POST /v1/events/{id}/redelivervuelve a enviar un evento a un endpoint, por ejemplo, tras una caída del servicio.
Rotación del secreto
Sección titulada «Rotación del secreto»POST /v1/webhook-endpoints/{id}/secret/rotate devuelve un secreto nuevo de inmediato. Durante las 24 horas
siguientes, cada entrega se firma con ambos secretos, el nuevo y el anterior (dos entradas en
webhook-signature), para que pueda desplegar el secreto nuevo sin perder eventos.