surtlyDocs
Cuenta y herramientas
GUÍA PARA PROVEEDORES

Recibe webhooks

Implementa los datos de eventos, la verificación de firmas y la recepción confiable.

Usa esta referencia al conectar tu propio sistema. Para configurar endpoints y suscripciones, comienza con Notificaciones.

Configura el receptor

Crea un endpoint HTTPS accesible que acepte solicitudes POST con JSON. En Notifications, guarda su URL, selecciona una versión de esquema y configura un secreto si el receptor verificará firmas. Después crea y habilita suscripciones para los eventos necesarios. Un endpoint activo por sí solo no suscribe eventos.

Editor de endpoint con controles de URL, secreto y selección de esquema
Haz coincidir el esquema y el secreto con el receptor antes de habilitar suscripciones a eventos.

Eventos y estructura del contenido

Cada cuerpo incluye schema_version, event_type y occurred_at (fecha de creación del evento). Los identificadores del evento y proveedor también se envían en encabezados. El contenido es una copia guardada del evento, no una lectura nueva del registro en cada reintento.

Tipos de eventoDatos de negocio en la raíz del cuerpo
order.created, order.fulfilled, order.cancelledorder
delivery.created, delivery.dispatched, delivery.completed, delivery.failed, delivery.cancelleddelivery, que contiene su order
payment.created, payment.refundedorder y payment

El evento Enroute de la interfaz usa delivery.dispatched; una entrega física exitosa usa delivery.completed. Trata los IDs como identificadores, no como contadores consecutivos. Los IDs de cliente pueden ser UUID.

Esquema 2026-08-17

La estructura de pedido siguiente aparece en order para eventos de pedido/pago y en delivery.order para eventos de entrega:

CamposContenido
id, created_at, transaction_type, cancelled, fulfilledIdentidad del pedido, fecha de creación, tipo de transacción y estados.
subtotal_cents, tax_cents, total_centsImportes monetarios guardados en centavos.
paymentpaid_cents, owed_cents y currency. Es el resumen del saldo del pedido.
clientid, name, email, tax_id, billing_address y metadata. Los datos opcionales de contacto pueden ser null.
providerid y name.
locationid y name del almacén que surte, o null.
delivery_locationDestino guardado, o null. Campos: name, address, notes, address_line1, address_line2, address_city, address_region, address_postal_code, contact_name, contact_phone, contact_email. Los valores individuales pueden ser null.
notesNotas del pedido, o null.
line_itemsLíneas con id, quantity, unit_price_cents, subtotal_cents, tax_cents, total_cents y product.
product de cada líneaid, name y metadata.

Los objetos metadata de cliente/producto usan las etiquetas de campos personalizados como claves y pueden estar vacíos. Lee la moneda enviada; no supongas que todos los proveedores usan MXN. Estos contenidos no incluyen archivos adjuntos.

Ejemplo de order.created con datos ficticios y sin destino de entrega guardado:

{
  "schema_version": "2026-08-17",
  "event_type": "order.created",
  "occurred_at": "2026-09-18T16:00:00.000Z",
  "order": {
    "id": 1042,
    "created_at": "2026-09-18T16:00:00.000Z",
    "transaction_type": "ORDER",
    "cancelled": false,
    "fulfilled": false,
    "subtotal_cents": 4000,
    "tax_cents": 0,
    "total_cents": 4000,
    "payment": {
      "paid_cents": 0,
      "owed_cents": 4000,
      "currency": "mxn"
    },
    "client": {
      "id": "00000000-0000-4000-8000-000000000011",
      "name": "Café Alba",
      "email": "compras@cafealba.example",
      "tax_id": null,
      "billing_address": "Calle Ejemplo 120, Ciudad Demo",
      "metadata": {}
    },
    "provider": {
      "id": 1,
      "name": "Distribuidora Demo"
    },
    "location": {
      "id": 1,
      "name": "Almacén Centro"
    },
    "delivery_location": null,
    "notes": "Preguntar por Elena.",
    "line_items": [
      {
        "id": 1,
        "quantity": 2,
        "unit_price_cents": 2000,
        "subtotal_cents": 4000,
        "tax_cents": 0,
        "total_cents": 4000,
        "product": {
          "id": 101,
          "name": "Agua mineral 600 ml",
          "metadata": {}
        }
      }
    ]
  }
}

Los eventos de entrega incluyen el pedido dentro de delivery, junto con id, created_at, updated_at, status y notes de la entrega, que puede ser null. Los estados de entrega son pending, dispatched, success, fail o cancelled.

Los eventos de pago incluyen un objeto payment separado en la raíz con id, kind, status, source, amount_cents, currency, external_object_type y external_object_id. Describen el registro de pago que originó el evento. Distingue ese objeto de order.payment, que resume el saldo del pedido. Las referencias externas pueden ser null en registros manuales.

Esquema 2026-05-14 y migración

Ambas versiones admiten los mismos tipos de evento y campos generales. Frente a 2026-08-17, el esquema anterior omite:

  • delivery_location y client.billing_address de todos los pedidos incluidos.
  • notes y line_items del pedido en eventos de entrega y pago. Los eventos de pedido conservan esos dos campos.

Haz que el receptor tolere campos null y campos adicionales desconocidos. Para migrar, agrega soporte para ambas versiones, selecciona la nueva en el editor del endpoint y verifica los valores recibidos de schema_version. Los envíos ya creados conservan su esquema anterior; no retires su soporte inmediatamente.

Encabezados de la solicitud

EncabezadoValor
content-typeapplication/json
x-enlazado-event-idIdentificador del evento, estable entre intentos.
x-enlazado-event-typeTipo de evento, por ejemplo order.created.
x-enlazado-provider-idID del proveedor como texto.
x-enlazado-timestampFecha ISO generada para este intento.
x-enlazado-signature-versionv1
x-enlazado-signatureHMAC-SHA256 hexadecimal; solo se incluye si el endpoint tiene un secreto.

Los nombres de encabezado conservan el prefijo enlazado. La fecha del intento difiere de occurred_at en el cuerpo y se regenera en los reintentos.

Verifica la firma

Calcula HMAC-SHA256 con el secreto configurado sobre la fecha del encabezado, un punto y los bytes exactos del cuerpo original. Compara la firma hexadecimal mediante una comparación de tiempo constante. Hazlo antes de interpretar el JSON: analizarlo y serializarlo de nuevo puede cambiar espacios o codificación e invalidar la firma.

Este ejemplo de Node.js espera un Buffer original y un mapa de encabezados en minúsculas, como los entrega el servidor HTTP de Node. La tolerancia de cinco minutos es una política recomendada para el receptor, no una restricción impuesta por Surtly. Mantén sincronizado el reloj del receptor.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(rawBody, headers, secret, now = Date.now()) {
  const timestamp = headers['x-enlazado-timestamp'];
  const signature = headers['x-enlazado-signature'];
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  if (headers['x-enlazado-signature-version'] !== 'v1') return false;
  if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;
  if (!/^[a-f0-9]{64}$/i.test(signature)) return false;

  const sentAt = Date.parse(timestamp);
  if (!Number.isFinite(sentAt) || Math.abs(now - sentAt) > 5 * 60_000) return false;

  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  const supplied = Buffer.from(signature, 'hex');
  return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}

Configura tu framework para conservar el cuerpo original antes de ejecutar el middleware de JSON. Si el receptor exige solicitudes firmadas, debe rechazar las que no tengan firma y usar un endpoint con secreto configurado. La firma cubre la fecha y el cuerpo; los demás encabezados no forman parte del contenido firmado. Valida el tipo de evento y el proveedor del cuerpo contra la integración configurada, en lugar de usar encabezados de enrutamiento sin firmar como autorización.

Después de verificar, interpreta el JSON y valida la versión y el tipo de evento admitidos. Guarda el evento de forma duradera o colócalo en una cola persistente antes de confirmar su recepción. Controla duplicados antes de repetir acciones de negocio.

Confirma y controla duplicados

Devuelve una respuesta HTTP 2xx en menos de 10 segundos para confirmar la recepción. Procesa el trabajo lento desde tu propia cola. Si aceptas un evento pero se pierde la respuesta, Surtly puede enviarlo otra vez; no supongas entrega única ni orden garantizado de eventos.

Usa el proveedor configurado y x-enlazado-event-id para registrar eventos aceptados. Responde 2xx a un duplicado ya aceptado sin repetir la acción. Los reintentos conservan ID y contenido, pero cambian la fecha del intento y su firma. Si varias suscripciones envían el mismo evento a tu sistema, decide si deben compartir ese registro de duplicados.

Reintentos y cambios de configuración

ResultadoComportamiento
HTTP 2xxEntregado; no hay otro intento automático.
HTTP 408, 425, 429 o 5xx; error de red o tiempo de espera agotadoSe reintenta mientras queden intentos.
Otro estado HTTP no exitosoFalla sin reintento automático.

Cada envío tiene como máximo cinco intentos en total: el inicial y hasta cuatro reintentos. Las esperas después de los fallos son aproximadamente 1 minuto, 5 minutos, 15 minutos y 60 minutos, contadas desde cada intento fallido. La tarea de reintentos revisa los pendientes cada minuto; son esperas programadas, no horas exactas de llegada. Tras el último intento fallido, el envío queda en Failed.

Cada envío guarda URL, secreto cifrado, versión de esquema y contenido. Actualizar o desactivar el endpoint o la suscripción actuales no cambia ni cancela los envíos ya en cola. Planea los cambios de URL y secreto para que el receptor todavía acepte envíos pendientes con la configuración anterior. La interfaz del proveedor no ofrece cancelación de cola ni reintento manual.

Usa el historial de eventos para revisar envíos individuales, número de intentos y últimos errores.

Las capturas utilizan datos ficticios. Los nombres de los controles corresponden a la aplicación.

En esta página