GSG Corp · Integraciones B2B

Manual de integración — Webhooks de estado de pedidos

Versión 1.1 · Actualizado el 2026-08-05 · Evento: route.status_changed

Este manual está dirigido al equipo técnico del integrador (CRM, e-commerce o ERP) que recibe notificaciones de GSG cuando un pedido registrado vía la API de integración cambia de estado logístico. Aquí está todo lo necesario para recibir, verificar y procesar esos eventos correctamente.

1.Novedades de esta versión (1.1)

El payload incorpora tres campos aditivos dentro de data: route_state_id, reported_at y received_at. Existen porque la app del motorizado ahora puede reportar estados sin señal: al recuperar conexión, los reportes acumulados se sincronizan en ráfaga y los webhooks pueden llegarte desordenados. La sección Orden de eventos explica la regla para procesarlos bien.

Acción requerida: si tu endpoint valida el payload contra un esquema estricto (por ejemplo JSON Schema con additionalProperties: false), actualízalo antes de la fecha de despliegue que te comunique GSG, o los eventos serán rechazados por tu propio validador. Si tu endpoint ignora campos desconocidos — el comportamiento recomendado — no necesitas hacer nada para seguir operando, aunque te conviene adoptar la regla de ordenamiento.

2.Cómo funciona

Cada vez que un pedido tuyo (creado vía POST /api/v1/orders/integration) cambia de estado logístico — asignado, en ruta, entregado, cancelado, etc. — GSG envía un POST JSON a la URL de webhook acordada. La entrega es asíncrona, con reintentos automáticos, y cada evento va firmado con HMAC-SHA256 usando el webhook_secret que GSG te entregó por canal seguro.

TransportePOST con cuerpo JSON a tu webhook_url (HTTPS)
Tipo de eventoroute.status_changed (versión de evento 1.0)
FirmaHMAC-SHA256 del cuerpo exacto, cabecera X-GSG-Signature
Respuesta esperadaCualquier código 2xx en menos de 10 segundos

3.Requisitos de tu endpoint

4.Cabeceras HTTP

CabeceraValor
Content-Typeapplication/json
X-GSG-Signaturesha256=<hex> — HMAC-SHA256 del cuerpo UTF-8 exacto, con clave webhook_secret.
X-GSG-Event-IdMismo UUID que event_id del cuerpo. Úsalo para deduplicar.
X-GSG-Event-Typeroute.status_changed
X-GSG-Event-Version1.0
User-AgentGSG-Webhook/1.0

5.Verificación de la firma

Lee el cuerpo como texto crudo (sin re-serializar el JSON), calcula el HMAC-SHA256 con tu secret y compara en tiempo constante con el valor que sigue a sha256=. Si no coincide, responde 401 y descarta.

Node.js (Express)

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

app.post("/webhooks/gsg", express.raw({ type: "application/json" }), (req, res) => {
  const firma = req.get("X-GSG-Signature") || "";
  const esperada = "sha256=" +
    createHmac("sha256", process.env.GSG_WEBHOOK_SECRET)
      .update(req.body)            // Buffer del cuerpo EXACTO recibido
      .digest("hex");

  const a = Buffer.from(firma), b = Buffer.from(esperada);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(401).end();
  }

  const evento = JSON.parse(req.body.toString("utf8"));
  res.status(200).end();           // responde rápido…
  procesarEvento(evento);          // …y procesa después
});

PHP

$body   = file_get_contents('php://input');
$firma  = $_SERVER['HTTP_X_GSG_SIGNATURE'] ?? '';
$secret = getenv('GSG_WEBHOOK_SECRET');

$esperada = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($esperada, $firma)) {
    http_response_code(401);
    exit;
}

$evento = json_decode($body, true);
http_response_code(200);
// procesar $evento (idealmente en una cola)

6.Payload del evento

{
  "event_id": "<uuid-v4>",
  "event_type": "route.status_changed",
  "event_version": "1.0",
  "occurred_at": "<ISO-8601>",
  "data": {
    "integration_client_code": "<tu código de integración>",
    "binding_id": 123,
    "store_token_ref": "st_…xxxxxx",
    "business_id": 456,
    "external_store_id": "tu-id-de-tienda",
    "route_id": 789,
    "tracking_code": "GSG-789",
    "order_external_id": "tu-id-de-pedido",
    "previous_status": "EN_RUTA",
    "new_status": "ENTREGADO",
    "status_changed_at": "<ISO-8601>",
    "route_state_id": 888994,
    "reported_at": "<ISO-8601 | null>",
    "received_at": "<ISO-8601>"
  }
}
CampoDescripción
event_idUUID único del evento. Clave de deduplicación.
order_external_idEl identificador del pedido en tu sistema, tal como lo enviaste al crearlo.
route_id / tracking_codeIdentificador y código de seguimiento del envío en GSG.
previous_status / new_statusEtiquetas legibles del estado (ENTREGADO, EN_RUTA, CANCELADO, …).
store_token_refReferencia opaca a la tienda (nunca se envía el token completo).
route_state_idv1.1 ID interno del cambio de estado, estrictamente creciente por route_id. Es tu llave de ordenamiento (sección 7). Puede ser null en flujos antiguos.
reported_atv1.1 Hora en que el motorizado registró el estado en su teléfono — la hora de negocio real de una entrega hecha sin señal. null en reportes online.
received_atv1.1 Hora en que el servidor de GSG registró el evento. La diferencia con reported_at es el tiempo que el reporte pasó sin conexión.

7.Orden de eventos y reportes offline

Los motorizados pueden operar en zonas sin cobertura: sus reportes se guardan en el teléfono y se sincronizan en ráfaga al recuperar señal. Sumado a los reintentos con backoff, esto significa que puedes recibir los webhooks de un mismo envío fuera de orden (por ejemplo: EN_RUTA, luego ENTREGADO, y al final un LLEGUE_A_DESTINO rezagado).

Regla de oro: guarda por cada route_id el máximo route_state_id procesado. Si llega un evento con un route_state_id menor o igual al guardado, es un evento viejo o un duplicado: ignóralo (respondiendo 2xx igualmente). Nunca uses las horas para ordenar — usa siempre route_state_id.
// pseudocódigo
ultimo = estadoPorRuta[evento.data.route_id] ?? 0

si (evento.data.route_state_id != null && evento.data.route_state_id <= ultimo) {
    // evento rezagado o duplicado → confirmar con 2xx y no aplicar
    return 200
}

aplicarCambioDeEstado(evento.data)
estadoPorRuta[evento.data.route_id] = evento.data.route_state_id
return 200

Si te interesa mostrar la hora real de la entrega (y no la hora de sincronización), usa reported_at cuando no sea null.

8.Reintentos y reenvíos

ParámetroValor
Timeout por intento10 segundos
Intentos automáticos8, con backoff exponencial (delay inicial 5 s)
ÉxitoCualquier respuesta 2xx
Agotados los intentosEl evento queda retenido en GSG y puede reenviarse manualmente más adelante

Ningún evento se pierde: si tu servidor estuvo caído, el equipo de GSG puede liberar los eventos retenidos cuando vuelvas a estar operativo. Esos reenvíos llegan con el mismo event_id original — otra razón por la que la deduplicación es obligatoria.

9.Buenas prácticas obligatorias

  1. Responde 2xx rápido y procesa en segundo plano. Un timeout cuenta como fallo y consume reintentos.
  2. Deduplica por event_id: reintentos y reenvíos entregan el mismo evento más de una vez.
  3. Ordena por route_state_id, nunca por hora de llegada ni por status_changed_at.
  4. Ignora campos que no conozcas: el contrato crece de forma aditiva. No valides con additionalProperties: false.
  5. Guarda el webhook_secret como credencial (vault/variables de entorno). Si sospechas que se filtró, pide a GSG rotarlo.
  6. Responde 2xx también a los eventos que decidas ignorar (viejos, duplicados, tipos que no te interesan). Un 4xx/5xx provoca reintentos inútiles.

10.Errores frecuentes

SíntomaCausa probable
La firma nunca coincide Estás re-serializando el JSON antes de firmar (cambia espacios/orden de claves). Calcula el HMAC sobre el cuerpo crudo recibido.
Dejaste de recibir eventos Tu endpoint respondió errores hasta agotar reintentos y los eventos quedaron retenidos, o el webhook fue desactivado. Contacta a GSG para reactivar y liberar los retenidos.
Estados “retroceden” en tu CRM Estás aplicando eventos en orden de llegada. Aplica la regla de route_state_id (sección 7).
Pedidos duplicados o inconsistentes Falta deduplicación por event_id.

11.Historial de cambios

VersiónFechaCambios
1.12026-08-05 Campos aditivos en data: route_state_id, reported_at, received_at. Nueva sección de ordenamiento para ráfagas offline (sección 7). Sin cambios en firma, cabeceras ni event_version.
1.02026-07-14 Versión inicial: evento route.status_changed, firma HMAC-SHA256, reintentos con backoff y reenvío de eventos retenidos.