Webhooks

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

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 ClientCampo eventCuándo se envía
PAYMENT_PAID · Pago confirmadopayment_coupon.paidUn cupón del negocio queda pagado. Excluye pagos de la facturación propia de 360studio.
CASE_CREATED · Ticket creadocase.createdSe crea un Ticket del negocio.
CASE_TRANSITIONED · Ticket cambió de estadocase.transitionedSe registra una transición de un Ticket.
SETTLEMENT_CREATED · Liquidación creadasettlement.createdSe crea una liquidación del negocio en estado IN_PROCESS.
SETTLEMENT_STATUS_CHANGED · Liquidación cambió de estadosettlement.status_changedUna 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.

Headers enviados

HeaderUso
Content-Typeapplication/json.
X-360Studio-Event-IdID estable del evento. Úsalo para idempotencia; varios webhooks suscritos al mismo evento comparten este ID.
X-360Studio-Delivery-IdID 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-IdID del webhook que origino el envio. Sirve para buscar el secreto correcto.
X-360Studio-TimestampFecha ISO UTC del intento, usada para firmar el payload. No es la fecha del evento.
X-360Studio-SignatureFirma HMAC-SHA256 en formato sha256=<hex>.
X-360Studio-AttemptNú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:

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.