Eventos de webhook
Coufai usa nombres de evento canónicos y estables. El nombre se entrega en el header X-Coufai-Event y en el campo event del cuerpo, siempre en el mismo formato (minúsculas, con puntos).
Los nombres de evento listados abajo son estables entre versiones. Puedes compararlos directamente: if (req.headers['x-coufai-event'] === 'transaction.paid') { ... }. Sin necesidad de normalización.
Cobros PIX (type: "transaction")
Eventos del ciclo de un cobro recibido.
| Evento | Cuándo ocurre |
|---|---|
transaction.paid | Pago confirmado y acreditado al saldo. |
transaction.failed | Falla en el pago (timeout, rechazo del pagador). |
transaction.cancelled | Cobro cancelado antes del pago. |
transaction.expired | Plazo expiró sin pago. |
transaction.refunded | Reembolso total. Saldo del comerciante debitado. |
transaction.partially_refunded | Reembolso parcial. |
transaction.infraction | MED (Mecanismo Especial de Devolución) abierto — saldo bloqueado preventivamente. |
transaction.dispute | Disputa abierta por el pagador. |
transaction.protest | Protesto registrado contra la transacción. |
transaction.blocked | Fondos bloqueados por orden judicial o compliance. |
transaction.chargeback | Chargeback aplicado — saldo debitado en definitivo. |
Retiros PIX (type: "transfer")
Eventos del ciclo de una transferencia (retiro) iniciada por tu API key o por el panel.
| Evento | Cuándo ocurre |
|---|---|
transfer.created | Retiro creado, esperando procesamiento. |
transfer.processing | Retiro en procesamiento en el banco. |
transfer.pending_approval | Retiro por encima del límite — espera aprobación manual interna. |
transfer.approved | Retiro aprobado manualmente, sigue al procesamiento. |
transfer.rejected | Retiro rechazado manualmente. Saldo devuelto. |
transfer.paid | Retiro concluido con éxito. |
transfer.failed | Retiro falló (clave PIX inválida, etc.). Saldo devuelto. |
transfer.cancelled | Retiro cancelado antes del procesamiento. Saldo devuelto. |
transfer.refunded | El destinatario devolvió el monto (PIX Reversal). Saldo reacreditado. |
transfer.partially_refunded | El destinatario devolvió parte. Saldo reacreditado proporcionalmente. |
Headers en cada POST
| Header | Descripción |
|---|---|
X-Coufai-Event | Nombre canónico del evento (p.ej. transaction.paid). |
X-Coufai-Timestamp | Timestamp Unix en segundos, parte de la firma. |
X-Coufai-Signature | sha256=<hex> — HMAC-SHA256 sobre ${timestamp}.${rawBody}. Ver Firma. |
X-Coufai-Event-Id | ID único del evento (evt_<ts>_<rand>). Úsalo para dedup entre múltiples URLs y reenvíos manuales. |
Campos opcionales (PIX)
En eventos donde el pago fue efectivamente liquidado, el webhook trae datos del PIX desde la red SPI/BACEN:
| Campo | Dónde aparece | Contenido |
|---|---|---|
endToEndId | transaction.paid / transaction.refunded / transaction.partially_refunded / transfer.paid / transfer.refunded / transfer.partially_refunded | E2E ID estándar BACEN (formato E<ISPB><YYYYMMDDhhmm><id>). Único por PIX. |
payer | transaction.* (PIX recibido — quién te pagó) | Objeto con name, document, documentType y bankAccount. |
beneficiary | transfer.* (PIX enviado — quién recibió tu retiro) | Objeto con name, document, documentType y bankAccount. |
refundEndToEndId | transfer.refunded / transfer.partially_refunded solamente | E2E ID del PIX de devolución (distinto del endToEndId del PIX original). |
Estructura de payer y beneficiary:
{
"name": "Maria Santos",
"document": "98765432100",
"documentType": "cpf",
"bankAccount": {
"ispb": "18236120",
"branch": "1",
"account": "914453272"
}
}Los campos payer.* y beneficiary.* contienen datos personales (nombre, CPF/CNPJ, cuenta bancaria). Trátalos como cualquier dato personal: guarda solo lo necesario, con acceso restringido, y respeta el derecho de eliminación del titular. Coufai entrega esos datos porque vienen de la red PIX — tú eres el controlador del tratamiento posterior.
Eventos que no traen esos campos: transaction.failed, transaction.cancelled, transaction.expired, transfer.created, transfer.processing, transfer.pending_approval, transfer.approved, transfer.rejected, transfer.failed, transfer.cancelled. Los campos se omiten del JSON cuando no están disponibles (no vienen como null).
Motivo del rechazo (retiros)
Cuando un retiro es rechazado, los eventos transfer.failed, transfer.rejected y transfer.cancelled pueden traer el motivo:
| Campo | Contenido |
|---|---|
refusedCode | Código estable del rechazo, en mayúsculas (ej.: INSUFFICIENT_BALANCE). Usa este campo en tu lógica — nunca parsees el mensaje. |
refusedReason | Descripción legible del motivo. Hoy se devuelve en portugués, y el texto puede cambiar. |
Los mismos campos aparecen en GET /v1/transactions/{id}.
Ambos son opcionales: cuando el motivo no está disponible, los campos se omiten del JSON. Trata la ausencia como “motivo no informado” — nunca como éxito.
Un retiro rechazado no se debita. Si el importe llegó a salir de tu saldo, se devuelve antes de enviar el evento — refusedReason siempre refleja el estado final.
Códigos más comunes:
refusedCode | Qué hacer |
|---|---|
INSUFFICIENT_BALANCE | Fallo temporal de liquidación. Reintenta en unos minutos. |
DAILY_LIMIT_EXCEEDED | Límite diario de retiros alcanzado. Reintenta al día siguiente. |
INVALID_PIX_KEY | Clave PIX inválida o no encontrada. Revisa los datos del destinatario antes de reintentar. |
La lista puede crecer. Trata un código desconocido como rechazo genérico — no asumas que la ausencia de un código conocido significa éxito.
Ejemplo: transfer.failed rechazado
{
"event": "transfer.failed",
"eventId": "evt_1715000000000_abcdef12",
"transactionId": "Q4t9aV...",
"status": "failed",
"amountCents": 100000,
"feeCents": 200,
"netCents": 100000,
"paidAt": null,
"timestamp": "1715000000",
"refusedCode": "DAILY_LIMIT_EXCEEDED",
"refusedReason": "Limite diário de saques atingido. O valor não foi debitado. Tente novamente amanhã."
}Ejemplo: transaction.paid
{
"event": "transaction.paid",
"eventId": "evt_1715000000000_abcdef12",
"transactionId": "Q4t9aV...",
"status": "paid",
"amountCents": 1000,
"feeCents": 8,
"netCents": 992,
"paidAt": "2026-05-08T03:30:00.000Z",
"timestamp": "1715000000",
"endToEndId": "E18236120202605080330abc123",
"payer": {
"name": "Maria Santos",
"document": "98765432100",
"documentType": "cpf",
"bankAccount": {
"ispb": "18236120",
"branch": "1",
"account": "914453272"
}
}
}Ejemplo: transaction.refunded
{
"event": "transaction.refunded",
"eventId": "evt_1715003600000_def456",
"transactionId": "Q4t9aV...",
"status": "refunded",
"amountCents": 1000,
"feeCents": 8,
"netCents": 992,
"paidAt": null,
"timestamp": "1715003600",
"endToEndId": "E18236120202605080330abc123",
"payer": {
"name": "Maria Santos",
"document": "98765432100",
"documentType": "cpf",
"bankAccount": {
"ispb": "18236120",
"branch": "1",
"account": "914453272"
}
}
}El monto del reembolso (parcial o total) se debita automáticamente de tu saldo Coufai. Sin acción adicional.
transaction.partially_refundedtiene el mismo schema.
Ejemplo: transfer.paid
{
"event": "transfer.paid",
"eventId": "evt_1715000300000_345678ab",
"transactionId": "...",
"status": "paid",
"amountCents": 5000,
"feeCents": 4,
"netCents": 4996,
"paidAt": "2026-05-08T03:35:00.000Z",
"timestamp": "1715000300",
"endToEndId": "E04838403202605080335ABCDEF",
"beneficiary": {
"name": "João Silva",
"document": "12345678901",
"documentType": "cpf",
"bankAccount": {
"ispb": "18236120",
"branch": "0001",
"account": "123456789"
}
}
}Ejemplo: transfer.refunded (devolución de retiro)
{
"event": "transfer.refunded",
"eventId": "evt_1715004000000_999",
"transactionId": "...",
"status": "transfer_refunded",
"amountCents": 5000,
"feeCents": 4,
"netCents": 4996,
"paidAt": null,
"timestamp": "1715004000",
"endToEndId": "E04838403202605080335ABCDEF",
"refundEndToEndId": "D18236120202605080500abc789",
"beneficiary": {
"name": "João Silva",
"document": "12345678901",
"documentType": "cpf",
"bankAccount": {
"ispb": "18236120",
"branch": "0001",
"account": "123456789"
}
}
}Cuando el destinatario devuelve un PIX que enviaste, el saldo se reacredita automáticamente.
transfer.partially_refundedtiene el mismo schema conamountCentsigual al valor parcialmente devuelto.
Ejemplo: transfer.failed
{
"event": "transfer.failed",
"eventId": "evt_1715000600000_9abc1234",
"transactionId": "...",
"status": "failed",
"amountCents": 5000,
"feeCents": 4,
"netCents": 4996,
"paidAt": null,
"timestamp": "1715000600"
}En retiros que terminan en
failed,cancelledorejected, el saldo se devuelve automáticamente. Sin acción adicional de tu lado más allá de marcar la operación. Estos eventos no traenendToEndIdnibeneficiary(no hubo liquidación).
Categorías para multi-webhook
Puedes configurar una URL por categoría o mezclar a tu gusto usando filtros de evento. Ver Configurando para el setup completo.
| Categoría | Eventos cubiertos |
|---|---|
cashin (pagos) | transaction.paid, transaction.failed, transaction.cancelled, transaction.expired |
refund (reembolsos) | transaction.refunded, transaction.partially_refunded |
dispute (disputas / holds) | transaction.infraction, transaction.dispute, transaction.protest, transaction.blocked |
chargeback | transaction.chargeback |
cashout (retiros) | transfer.* (los 10 eventos) |