Guía de webhooks salientes
Los webhooks notifican a tu backend sobre pagos confirmados, Tickets y liquidaciones. Cada suscripción pertenece al negocio y recibe los eventos del tipo elegido de todos sus Workspaces. Los eventos de Ticket incluyen workspace_id; los eventos de pago y liquidaciones no incluyen ese campo.
Configurar desde Client
Selecciona el negocio en Client y abre Administración > API y webhooks > Webhooks. También puedes acceder desde el resumen de configuración del negocio. Usa Nuevo webhook para elegir el evento y registrar la URL HTTPS de tu backend.
La gestión de webhooks requiere una sesión de usuario y permisos del negocio en Client. La API para integraciones usa /v1 y no ofrece métodos para crear, editar, rotar o reintentar webhooks. Esta guía describe los avisos que recibe tu servidor.
Para ver la pestaña y el historial necesitas webhooks.read (Consultar webhooks). Crear, editar, probar y rotar secretos requiere además webhooks.manage (Administrar webhooks). Si no ves esas acciones, solicita los permisos al administrador del negocio.
Los partners también pueden registrar estos eventos en el array hooks al crear un negocio mediante su API o seleccionarlos en el formulario de alta del Portal Partners. La respuesta de creación devuelve los hook_signing_secrets una sola vez.
Flujo de entrega
- Creas una suscripción activa para uno de los eventos disponibles.
- 360studio genera un
signing_secretúnico por webhook y lo muestra una sola vez. Guárdalo en tu backend. - Cuando ocurre un evento del tipo elegido, 360studio persiste una entrega y la envía mediante
POSTa la URL configurada, con JSON y firma HMAC-SHA256. - Tu endpoint verifica la firma, registra el evento de forma durable y responde con un código
2xx. Procesa después el trabajo que tome más tiempo.
La URL debe ser HTTPS pública en el puerto 443, sin credenciales ni fragmento. Se rechazan destinos locales, privados o reservados, también al resolver DNS. No se siguen redirecciones: una respuesta 3xx cuenta como fallo.
Eventos disponibles
| Tipo en Client | Campo event | Cuándo se envía |
|---|---|---|
PAYMENT_PAID · Pago confirmado | payment_coupon.paid | Un cupón del negocio queda pagado. Excluye pagos de la facturación propia de 360studio. |
CASE_CREATED · Ticket creado | case.created | Se crea un Ticket del negocio. |
CASE_TRANSITIONED · Ticket cambió de estado | case.transitioned | Se registra una transición de un Ticket. |
SETTLEMENT_CREATED · Liquidación creada | settlement.created | Se crea una liquidación del negocio en estado IN_PROCESS. |
SETTLEMENT_STATUS_CHANGED · Liquidación cambió de estado | settlement.status_changed | Una liquidación cambia entre IN_PROCESS, COMPLETED y REJECTED. Repetir el mismo estado no genera otro evento. |
La acción Enviar prueba del detalle del webhook encola un evento sintético webhook.test firmado. No crea ni paga cupones y no modifica Tickets. La aceptación de la prueba no confirma su entrega: consulta el historial para verificar el resultado.
Datos de integración de cupones
Consulta el prefijo real del negocio y los IDs de sus Workspaces accesibles en Client > Administración > API y webhooks > API keys. Guarda la API key únicamente en tu backend como STUDIO_API_KEY, el Workspace como STUDIO_WORKSPACE_ID y el prefijo como STUDIO_PAYMENT_PREFIX. Estos nombres de variables son una sugerencia para tu aplicación.
La API key identifica al Business. POST /v1/payment-coupons requiere el header x-workspace-id, incluso con una clave autorizada para un único Workspace. workspace_id en el body no sustituye ese header. No solicites el ID a la persona que paga.
Usa los scopes payment_coupons:write y payment_coupons:read, un Workspace activo con Payments habilitado y el servicio operativo. Utiliza payment_url y bank_deeplinks de la respuesta tal como se reciben.
Reintentos e idempotencia
El ciclo automático permite un intento inicial y hasta 3 reintentos con espera progresiva. El envío HTTP tiene un límite de 10 segundos. Los errores de red, timeouts y respuestas fuera de 2xx, incluidos 4xx, cuentan como fallos.
La entrega tiene semántica at-least-once: el mismo evento puede llegar más de una vez, incluso si tu sistema lo procesó pero 360studio no recibió la confirmación. No presupongas un orden de llegada entre eventos.
- Deduplica con
X-360Studio-Event-Iddentro del negocio receptor, de forma atómica con tus efectos o con el registro durable del trabajo. - Si recibes un evento ya aceptado, responde
200sin repetir efectos secundarios. - Los reintentos automáticos y manuales conservan los IDs de evento y entrega. Cada intento usa un timestamp y una firma nuevos.
- No dependas del número de intento para decidir el estado del recurso; usa el estado y los IDs del payload.
Headers enviados
| Header | Uso |
|---|---|
Content-Type | application/json. |
X-360Studio-Event-Id | ID estable del evento. Úsalo para idempotencia; varios webhooks suscritos al mismo evento comparten este ID. |
X-360Studio-Delivery-Id | ID de la entrega para un webhook y evento. Se conserva en reintentos automáticos y manuales; otro webhook recibe una entrega con un ID distinto. |
X-360Studio-Webhook-Id | ID del webhook que origino el envio. Sirve para buscar el secreto correcto. |
X-360Studio-Timestamp | Fecha ISO UTC del intento, usada para firmar el payload. No es la fecha del evento. |
X-360Studio-Signature | Firma HMAC-SHA256 en formato sha256=<hex>. |
X-360Studio-Attempt | Número de intento, desde 1. Sigue aumentando después de un reintento manual. |
Verificar la firma
La firma se calcula sobre timestamp + "." + rawBody usando el signing_secret completo del webhook. No uses la API key ni la vista parcial del secreto. Verifica antes de procesar el JSON.
En este ejemplo Node.js, rawBody es el Buffer recibido sin modificar y headers contiene los nombres en minúsculas. Con Express, registra express.raw({ type: 'application/json' }) para tu receptor antes de express.json(). No vuelvas a serializar un objeto JSON para verificarlo.
import crypto from 'node:crypto';
function verify360StudioWebhook(rawBody, headers, signingSecret) {
const timestamp = headers['x-360studio-timestamp'];
const signature = headers['x-360studio-signature'];
if (!Buffer.isBuffer(rawBody)) return false;
if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;
if (!/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
const timestampMs = Date.parse(timestamp);
if (!Number.isFinite(timestampMs)) return false;
const ageMs = Math.abs(Date.now() - timestampMs);
if (ageMs > 5 * 60 * 1000) return false;
const expected = crypto
.createHmac('sha256', signingSecret)
.update(timestamp + '.', 'utf8')
.update(rawBody)
.digest();
return crypto.timingSafeEqual(
Buffer.from(signature.slice(7), 'hex'),
expected
);
}
La ventana de 5 minutos es una política recomendada para tu receptor; mantén su reloj sincronizado. Rechaza firmas inválidas y combina la validación temporal con la deduplicación persistida del evento.
Payloads predeterminados
Los siguientes ejemplos muestran el body JSON que emite cada tipo de webhook sin personalización. Los IDs, referencias, fechas e importes son ficticios; la estructura y los campos corresponden al servicio implementado. Los eventos de negocio incluyen version: 1 y occurred_at.
PAYMENT_PAID · Pago confirmado
{
"event": "payment_coupon.paid",
"version": 1,
"occurred_at": "2026-10-15T15:30:00.000Z",
"data": {
"payment_coupon_id": "68b1f0000000000000000001",
"payment_reference": "A1B2C3D4E5F6G",
"external_ref": "pedido-demo-001",
"amount": 150.5,
"currency": "PEN",
"paid_at": "2026-10-15T15:30:00.000Z"
}
}
El payload de pago contiene payment_coupon_id, payment_reference, amount, currency y paid_at; external_ref puede omitirse. No incluye workspace_id ni el número de transacción bancaria. Una simulación de pago en sandbox añade data.simulated: true. Correlaciona el aviso con el cupón que guardaste al crearlo.
CASE_CREATED · Ticket creado
{
"event": "case.created",
"version": 1,
"occurred_at": "2026-10-15T15:30:00.000Z",
"data": {
"case_id": "68b1f0000000000000000002",
"workspace_id": "68b1f0000000000000000003",
"contact_id": "68b1f0000000000000000004",
"type_id": "68b1f0000000000000000005",
"state_id": "68b1f0000000000000000006",
"status": "open",
"source": "manual",
"value": 150.5,
"currency": "PEN",
"responsible_user_id": "68b1f0000000000000000012"
}
}
El ejemplo incluye los campos opcionales value, currency y responsible_user_id; se omiten cuando el Ticket no tiene esos datos.
CASE_TRANSITIONED · Ticket cambió de estado
{
"event": "case.transitioned",
"version": 1,
"occurred_at": "2026-10-15T15:35:00.000Z",
"data": {
"case_id": "68b1f0000000000000000002",
"transition_id": "68b1f0000000000000000007",
"workspace_id": "68b1f0000000000000000003",
"contact_id": "68b1f0000000000000000004",
"from_state_id": "68b1f0000000000000000006",
"to_state_id": "68b1f0000000000000000008",
"from_version": 1,
"to_version": 2,
"status": "closed",
"origin": "manual",
"classification": "completed",
"outcome": "positive",
"order_id": "68b1f0000000000000000013",
"reason": "Venta completada"
}
}
El ejemplo muestra el cierre positivo de un Ticket con una orden asociada. classification, outcome, order_id y reason son opcionales; también puede incluir outcome_reason_id cuando se registra un motivo de resultado. Los IDs de tipo y estado y la clasificación corresponden a la configuración del Workspace.
SETTLEMENT_CREATED · Liquidación creada
Los dos eventos de liquidaciones incluyen settlement_id, business_id, currency, payment_coupons_count, total_amount, commission_amount, total_paid y status. La moneda es PEN; batch_id se incluye cuando existe. Los importes corresponden al snapshot persistido de la liquidación, sin cuentas bancarias ni datos personales.
{
"event": "settlement.created",
"version": 1,
"occurred_at": "2026-10-15T15:30:00.000Z",
"data": {
"settlement_id": "68b1f0000000000000000010",
"business_id": "68b1f0000000000000000011",
"batch_id": "settlement-batch-demo",
"currency": "PEN",
"payment_coupons_count": 3,
"total_amount": 150.5,
"commission_amount": 4.5,
"total_paid": 146,
"status": "IN_PROCESS"
}
}
Una liquidación nueva se notifica en estado IN_PROCESS y no incluye previous_status.
SETTLEMENT_STATUS_CHANGED · Liquidación cambió de estado
settlement.status_changed añade previous_status. Cada notificación corresponde a una liquidación y se entrega solo a los webhooks de su negocio, incluso cuando se actualiza un lote de varios negocios.
{
"event": "settlement.status_changed",
"version": 1,
"occurred_at": "2026-10-15T15:35:00.000Z",
"data": {
"settlement_id": "68b1f0000000000000000010",
"business_id": "68b1f0000000000000000011",
"batch_id": "settlement-batch-demo",
"currency": "PEN",
"payment_coupons_count": 3,
"total_amount": 150.5,
"commission_amount": 4.5,
"total_paid": 146,
"previous_status": "IN_PROCESS",
"status": "COMPLETED"
}
}
Entrega de prueba
{
"event": "webhook.test",
"version": 1,
"test": true,
"hook_id": "68b1f0000000000000000009",
"sent_at": "2026-10-15T15:40:00.000Z"
}
El evento de prueba usa sent_at en lugar de occurred_at y no lleva un objeto data.
En la configuración avanzada de Client puedes personalizar el JSON: payload_mapping mueve campos de una ruta de origen a una de destino (por ejemplo, data.amount a payment.amount); include_unmapped_fields conserva los demás campos por defecto y false los excluye; additional_fields añade o reemplaza campos del nivel superior. Esto también se aplica a las pruebas. La firma siempre cubre el JSON final enviado.
Consultar entregas y reintentar
En Client > Administración > API y webhooks > Webhooks, selecciona el webhook y abre una entrega del historial. El detalle muestra su estado, intentos, código HTTP, error y una vista limitada de la respuesta del receptor.
Reintentar está disponible con permisos de gestión para entregas dead (Fallido) o enqueue_failed (Error de cola). El reintento vuelve a encolar la misma entrega y habilita hasta 4 intentos adicionales; conserva su payload, URL, headers y secreto originales. Una entrega ya marcada como delivered no se reintenta manualmente.
Correlaciona cada aviso recibido con X-360Studio-Event-Id y X-360Studio-Delivery-Id. No uses el número de intento como identificador de idempotencia.
Dónde rotar credenciales
En Client > Administración > API y webhooks se administran dos credenciales independientes:
- API key: pestaña API keys, elige una credencial activa y pulsa Rotar. Necesitas
api_keys.read,api_keys.managey los permisos asociados a los scopes de esa credencial. La rotación crea una clave de reemplazo y revoca inmediatamente la anterior. Copia el nuevo valor, que solo se muestra una vez, y actualiza la credencial que usa tu backend para llamar a/v1. - Secreto HMAC del webhook: pestaña Webhooks, selecciona el webhook y pulsa Rotar secreto. Necesitas
webhooks.readywebhooks.manage. Copia el nuevosigning_secret, que solo se muestra una vez, y actualiza el verificador de firmas de tu backend.
Rotar la API key no cambia el secreto HMAC y rotar el secreto HMAC no cambia la API key. Las consultas posteriores solo muestran una vista parcial de cada credencial; si pierdes el valor completo, debes rotarlo.
Entregas pendientes al rotar: el secreto nuevo se aplica a entregas creadas después de la rotación. Las ya creadas conservan el secreto anterior, incluso en reintentos posteriores. Durante una rotación planificada, permite verificar con el secreto nuevo y el anterior hasta resolver esas entregas; no hay un plazo fijo de expiración del secreto guardado en ellas. Si el motivo es un compromiso del secreto, no mantengas su aceptación: revisa las entregas afectadas y concilia los eventos rechazados.