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.
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.
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.
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.
| Transporte | POST con cuerpo JSON a tu webhook_url (HTTPS) |
|---|---|
| Tipo de evento | route.status_changed (versión de evento 1.0) |
| Firma | HMAC-SHA256 del cuerpo exacto, cabecera X-GSG-Signature |
| Respuesta esperada | Cualquier código 2xx en menos de 10 segundos |
POST con Content-Type: application/json por HTTPS.event_id: el mismo evento puede llegar más de una vez.| Cabecera | Valor |
|---|---|
Content-Type | application/json |
X-GSG-Signature | sha256=<hex> — HMAC-SHA256 del cuerpo UTF-8 exacto, con clave webhook_secret. |
X-GSG-Event-Id | Mismo UUID que event_id del cuerpo. Úsalo para deduplicar. |
X-GSG-Event-Type | route.status_changed |
X-GSG-Event-Version | 1.0 |
User-Agent | GSG-Webhook/1.0 |
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.
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
});
$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)
{
"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>"
}
}
| Campo | Descripción |
|---|---|
event_id | UUID único del evento. Clave de deduplicación. |
order_external_id | El identificador del pedido en tu sistema, tal como lo enviaste al crearlo. |
route_id / tracking_code | Identificador y código de seguimiento del envío en GSG. |
previous_status / new_status | Etiquetas legibles del estado (ENTREGADO, EN_RUTA, CANCELADO, …). |
store_token_ref | Referencia 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. |
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).
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.
| Parámetro | Valor |
|---|---|
| Timeout por intento | 10 segundos |
| Intentos automáticos | 8, con backoff exponencial (delay inicial 5 s) |
| Éxito | Cualquier respuesta 2xx |
| Agotados los intentos | El 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.
event_id: reintentos y reenvíos entregan el mismo evento más de una vez.route_state_id, nunca por hora de llegada ni por status_changed_at.additionalProperties: false.webhook_secret como credencial (vault/variables de entorno). Si sospechas que se filtró, pide a GSG rotarlo.| Síntoma | Causa 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. |
| Versión | Fecha | Cambios |
|---|---|---|
| 1.1 | 2026-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.0 | 2026-07-14 | Versión inicial: evento route.status_changed, firma HMAC-SHA256,
reintentos con backoff y reenvío de eventos retenidos. |