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, así 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
alcance 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 usar https://. Un endpoint pertenece a un solo 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 creó el sobre |
envelope.ready |
El documento está generado y el sobre está listo para sus destinatarios |
recipient.sent |
Llegó el turno de un destinatario (y, con la entrega por correo, se envió su enlace) |
recipient.viewed |
Un destinatario abrió la página de firma por primera vez |
recipient.signed |
Un destinatario envió su firma (o se contrafirmó automáticamente) |
recipient.declined |
Un destinatario rechazó la firma |
envelope.completed |
Todos los destinatarios terminaron. data.values contiene las variables y cada valor ingresado |
envelope.sealed |
El PDF sellado está listo para descargar |
envelope.declined |
El sobre finalizó porque un destinatario rechazó la firma |
envelope.voided |
Usted anuló el sobre |
envelope.expired |
expiresAt pasó antes de completarse |
signing_link.used |
Un visitante inició un sobre desde un enlace de firma permanente |
acceptance.created está reservado en el esquema y todavía no se emite.
Carga útil
Sección titulada “Carga útil”{ "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 usted envió y los valores de los campos de cada rol por ID de campo.
Encabezados y firma
Sección titulada “Encabezados y firma”Cada entrega incluye:
| Encabezado | Valor |
|---|---|
webhook-id |
El ID del evento (evt_…); es el mismo en cada reintento. Úselo para deduplicar |
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}, con la parte del secreto que sigue a
whsec_, decodificada en base64, como clave. Verifique el cuerpo sin procesar (antes de analizar el JSON),
compare en tiempo constante y rechace las marcas de tiempo que difieran en 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 dentro de los 10 segundos. No se siguen las redirecciones.
Todo lo demás se reintenta con espera progresiva, después de 5 segundos, 5 minutos, 30 minutos, 2 horas, 5 horas,
10 horas, 14 horas, 20 horas y 24 horas, durante un máximo de tres días después del evento. Luego, la entrega se
marca como fallida.
- Responda rápido y procese de forma asíncrona; deduplique por
webhook-id. - Las entregas pueden llegar desordenadas: compruebe
data.envelope.statusen lugar de suponer una secuencia. GET /v1/webhook-endpoints/{id}/deliveriesenumera los intentos recientes con códigos de estado, errores y tiempos, y cada endpoint muestra su tasa de éxito de las últimas 24 horas (health).POST /v1/events/{id}/redelivervuelve a enviar un evento a un endpoint, por ejemplo, después de una interrupción 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 implementar el secreto nuevo sin perder eventos.