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
{
"event": "ticket.paid",
"_id": "64b1f...",
"external_ref": "ORD-9988",
"amount": 150.5,
"status": "paid",
"paid_at": "2026-05-30T12:30:00.000Z",
"bank_tx_id": "BCP-123456"
}
El payload puede variar si el hook usa payload_mapping, include_unmapped_fields o additional_fields.
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"
}
}