Guia de webhooks de pago
Los webhooks notifican a tu backend cuando un cupon cambia a pagado. La entrega es asincronica: 360Pay confirma el pago y luego intenta enviar el evento a tu URL.
Flujo de entrega
- Configuras un hook con tipo
PAYMENT_PAIDy una URL HTTPS. - 360Pay genera un
signing_secretunico para ese hook y lo muestra una sola vez. - Cuando el cupon pasa a
PAID, 360Pay crea una entrega durable y la procesa con Cloud Tasks. - Tu endpoint debe responder rapido con cualquier codigo
2xxpara marcar la entrega como exitosa.
Crear hook y guardar secreto
No envies signing_secret al crear un hook. 360Pay lo genera, lo cifra en el servidor y lo devuelve una sola vez para que tu backend pueda verificar X-360Pay-Signature.
POST /app/v1/hooks
{
"type": "PAYMENT_PAID",
"url": "https://api.mibusiness.com/webhook",
"active": true
}
Reintentos e idempotencia
360Pay hace un intento inicial y hasta 3 reintentos. La entrega tiene semantica at-least-once: el mismo evento puede llegar mas de una vez si hubo timeout, error de red o respuesta no 2xx.
- Guarda
X-360Pay-Event-Idy procesa cada evento una sola vez. - Si recibes un evento ya procesado, responde
200sin repetir efectos secundarios. - No dependas del numero de intento para decidir si el pago es valido; usa el estado y los IDs del payload.
Headers enviados
| Header | Uso |
|---|---|
X-360Pay-Event-Id | ID estable del evento. Usalo para idempotencia. |
X-360Pay-Delivery-Id | ID de una entrega especifica. Cambia entre reintentos manuales o nuevas entregas. |
X-360Pay-Hook-Id | ID del hook que origino el envio. Sirve para buscar el secreto correcto. |
X-360Pay-Timestamp | Fecha ISO usada para firmar el payload. |
X-360Pay-Signature | Firma HMAC-SHA256 en formato sha256=<hex>. |
X-360Pay-Attempt | Numero de intento de entrega. |
Verificar la firma
La firma se calcula sobre timestamp + "." + rawBody. Debes usar el body crudo recibido, no un objeto JSON vuelto a serializar.
import crypto from 'crypto';
function verify360PayWebhook(rawBody, headers, signingSecret) {
const timestamp = headers['x-360pay-timestamp'];
const signature = headers['x-360pay-signature'];
if (!timestamp || !signature) return false;
const ageMs = Math.abs(Date.now() - new Date(timestamp).getTime());
if (ageMs > 5 * 60 * 1000) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', signingSecret)
.update(timestamp + '.' + rawBody, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Payload ejemplo
El cuerpo siempre es un array, incluso cuando el banco paga un solo cupon. Una transaccion con varios documentos produce una sola entrega con todos sus cupones completos en el mismo array.
[
{
"event": "ticket.paid",
"_id": "64b1f...",
"external_ref": "ORD-9988",
"payment_reference": "A1B2C3D4E5F6G7",
"amount": 50,
"status": "paid",
"paid_at": "2026-08-26T10:00:00.000Z",
"operation_number": "OPERACION-BANCO-123",
"bank_tx_id": "BCP"
},
{
"event": "ticket.paid",
"_id": "64b20...",
"external_ref": "ORD-9989",
"payment_reference": "CUPON-002",
"amount": 75,
"status": "paid",
"paid_at": "2026-08-26T10:00:00.000Z",
"operation_number": "OPERACION-BANCO-123",
"bank_tx_id": "BCP"
}
]
Cada elemento puede variar si el hook usa payload_mapping, include_unmapped_fields o additional_fields.
Si el cupón ya estaba pagado y luego se completa/corrige el codigo bancario (por ejemplo operation_number o bank_tx_id), 360Pay puede emitir un nuevo evento PAYMENT_PAID con un X-360Pay-Event-Id distinto para reflejar la actualizacion.
Usa un unico hook PAYMENT_PAID para pagos de uno o varios cupones. El identificador del evento permanece estable durante los reintentos de la misma operacion.
Donde se configuran
- Negocios que usan la app pueden crear y rotar hooks desde
/app/v1/hooks. - Partners pueden enviar hooks al crear negocios con
POST /partners/v1/businesses. La respuesta incluyehook_signing_secretsuna sola vez. - Partners pueden rotar secretos con
POST /partners/v1/businesses/:businessId/hooks/:hookId/rotate-secret. - Si pierdes el secreto, rota el hook y guarda el nuevo valor en tu backend.
Consultar entregas y obtener deliveryId
Desde App API puedes listar el historial de entregas de un hook. El campo _id de cada item es el deliveryId que se usa para consultar detalle o reintentar una entrega fallida.
GET /app/v1/hooks/:hookId/deliveries?page=1&limit=10
{
"success": true,
"data": [
{
"_id": "665ab1200f8a4f0012c3d459",
"hook_id": "665aa0100f8a4f0012c3d458",
"event_id": "evt_0f8c7f3a9d6e4b1a2c3d4e5f6a7b8c9d",
"event_type": "PAYMENT_PAID",
"resource_type": "coupon",
"resource_id": "665ab0f00f8a4f0012c3d450",
"status": "dead",
"attempt_count": 4,
"max_attempts": 4,
"last_http_status": 500,
"last_error": "Webhook responded with HTTP 500",
"created_at": "2026-05-30T12:30:00.000Z",
"updated_at": "2026-05-30T12:40:00.000Z"
}
],
"meta": {
"page": 1,
"limit": 10,
"total": 1,
"total_pages": 1
}
}
Con ese deliveryId puedes consultar o reintentar:
GET /app/v1/hooks/:hookId/deliveries/665ab1200f8a4f0012c3d459
POST /app/v1/hooks/:hookId/deliveries/665ab1200f8a4f0012c3d459/retry
Respuesta al crear hook desde App API
El secreto completo solo aparece en la respuesta de creacion. Las consultas posteriores muestran solo una vista parcial.
{
"success": true,
"data": {
"_id": "665aa0100f8a4f0012c3d458",
"business_id": "665a9f9f0f8a4f0012c3d456",
"type": "PAYMENT_PAID",
"url": "https://api.mibusiness.com/webhook",
"active": true,
"signing_secret_preview": "whsec_VsLSN...y1A",
"signing_secret": "whsec_VsLSN43xjcHqk4UoW6Fv5P8v3v8YGxQ2pLkV9fR8y1A"
}
}
Respuesta al crear negocio con hooks desde Partner API
Cuando el partner crea un negocio y envia hooks, cada hook recibe su propio secreto. Guarda cada secreto asociado a su hook_id.
{
"success": true,
"data": {
"success": true,
"business_id": "665a9f9f0f8a4f0012c3d456",
"payment_prefix": "EMP",
"config_id": "665aa0040f8a4f0012c3d457",
"hook_ids": ["665aa0100f8a4f0012c3d458"],
"hook_signing_secrets": [
{
"hook_id": "665aa0100f8a4f0012c3d458",
"signing_secret": "whsec_VsLSN43xjcHqk4UoW6Fv5P8v3v8YGxQ2pLkV9fR8y1A"
}
]
},
"message": "Business created and linked to partner successfully"
}
Respuesta al rotar secreto
La rotacion invalida el secreto anterior para futuros envios y muestra el nuevo valor una sola vez. En App API usa POST /app/v1/hooks/:hookId/rotate-secret; en Partner API usa POST /partners/v1/businesses/:businessId/hooks/:hookId/rotate-secret.
{
"success": true,
"data": {
"_id": "665aa0100f8a4f0012c3d458",
"type": "PAYMENT_PAID",
"url": "https://api.mibusiness.com/webhook",
"active": true,
"signing_secret_preview": "whsec_dF7a...Q9p",
"signing_secret": "whsec_dF7abpQ1Lk2N9s4xRxu6Qv6M0h3J6zK5uGfQ9p"
}
}