Este manual está dirigido al equipo técnico del integrador (plataforma multi-tienda, CRM, e-commerce, ERP o sistema propio del negocio) 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 cuatro campos aditivos dentro de data:
state_id, route_type_code, size_code y service.
Con ellos tu sistema sabe, sin consultar el pedido, qué estado numérico se aplicó y qué tipo de
envío y servicio (courier o GSG XL) era. event_version se mantiene en 1.0:
la política del contrato es aditiva.
Además, el manual cubre el nuevo escenario de integración directa: el negocio con sistema propio configura su propio webhook desde el panel GSG (sección 3).
Revisión del mismo día (2026-09-03): una respuesta 3xx de tu endpoint ya no se reintenta (el evento queda retenido de inmediato, sección 9);
los estados DRIVER_ASIGNADO y DRIVER_DESASIGNADO ahora también generan evento; previous_status es null
(no la cadena "null") cuando no hay estado previo; route_state_id viaja en todos los caminos de cambio de estado; y los eventos generados
mientras el vínculo de la tienda no estaba activo quedan retenidos en GSG y pueden liberarse al reactivarlo (sección 9).
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.
Cada vez que un pedido tuyo (creado vía POST /api/v1/orders/integration)
cambia de estado — asignado, en ruta, entregado, cancelado, etc. — GSG envía un
POST JSON a tu URL de webhook. La entrega es asíncrona, con
reintentos automáticos, y cada evento va firmado con HMAC-SHA256 usando el
webhook_secret que obtuviste al configurar el webhook.
| 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 |
| Escenario | Quién configura | Alcance |
|---|---|---|
| Plataforma multi-tienda | Un administrador de GSG, a pedido del integrador (URL y activación). El webhook_secret se entrega por canal seguro al crear o rotar. |
Una URL por plataforma. Recibes los eventos de todas las tiendas vinculadas; distingue la tienda con binding_id, external_store_id o business_id. |
| Integración directav1.2 | El propio negocio, desde su panel GSG (Integraciones → Integración directa → Webhook): URL, activar/desactivar y rotar el secret. Puede indicar la URL ya en la solicitud: el webhook_secret se muestra una sola vez en la respuesta de la solicitud; después solo al rotarlo (sección Webhook → «Rotar el secret», o rotate_webhook_secret: true al regenerar la API key). |
Una URL por negocio. Todos los eventos corresponden a ese negocio. |
localhost ni redes privadas), sin credenciales embebidas, máximo 500 caracteres.route.status_changed. Si no indicas suscripción, recibes todos los soportados.GET /api/v1/integration/me devuelve el estado de tu webhook (configured, is_active, url_masked, last_delivery_at) para verificar la configuración desde tu backend (solo integración directa; para plataformas es null).DRIVER_ASIGNADO (6) y DRIVER_DESASIGNADO (18).POST con Content-Type: application/json por HTTPS en un host público (no se aceptan URLs http://, localhost ni redes privadas).Redirección <status> a <location>: GSG no sigue redirecciones; registre la URL final. Registra la URL final (https, sin barra final que redirija, dominio canónico). Si tu procesamiento es pesado, encola y responde primero.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-E-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>",
"state_id": 14,
"route_type_code": "DELIVERY",
"size_code": "M",
"service": "COURIER"
}
}
| 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 | Códigos del estado (ENTREGADO, EN_RUTA, CANCELADO, INACTIVO, …). Catálogo completo en Seguimiento § 5. previous_status es null (JSON null, nunca la cadena "null") cuando no hay estado previo, por ejemplo en una corrección manual de soporte. |
store_token_ref | Referencia opaca a la tienda (nunca se envía el token completo). |
binding_id, external_store_id, business_id | Identifican la tienda/negocio. En una plataforma multi-tienda úsalos para enrutar el evento a la tienda correcta. |
route_state_idv1.1 |
ID interno del cambio de estado, estrictamente creciente por route_id. Es tu llave de ordenamiento (sección 8). Desde esta revisión viaja en todos los caminos de cambio de estado (asignación masiva, optimización, reportes del motorizado, correcciones); sigue tolerando null en tu validador por compatibilidad. Coincide con current_state.route_state_id del endpoint de consulta. |
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. |
state_idv1.2 |
Id numérico del nuevo estado (el mismo id del catálogo states; p. ej. 14 = ENTREGADO). Úsalo si prefieres mapear por número en vez de por new_status. Puede ser null en flujos antiguos. |
route_type_codev1.2 |
Tipo de envío: DELIVERY, AGENCY, EXCHANGE, GSG_OFFICE, FLEX_DELIVERY. null si no se pudo resolver. |
size_codev1.2 |
Tamaño canónico del bulto (S, M, L, XL_GRANDE, XL_PLUS, XL_MAX; XL_STD solo en pedidos históricos). Si registraste con size: "XL" aquí llega la categoría que GSG asignó. null en pedidos antiguos sin tamaño canónico. |
servicev1.2 |
COURIER o XL. |
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 EN_EL_PUNTO 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 |
| Respuesta 3xx | Retenido de inmediato, sin reintentos (GSG no sigue redirecciones). Corrige la URL registrada y pide el reenvío |
| Agotados los intentos | El evento queda retenido en GSG y puede reenviarse manualmente más adelante |
| Vínculo de la tienda no activo | Si la tienda estaba desvinculada, bloqueada o pendiente cuando ocurrió el cambio de estado, el evento se guarda retenido (sin intentar la entrega). Al reactivarse el vínculo, GSG puede liberarlo |
| Webhook desactivado | No se generan eventos nuevos; los ya encolados se retienen sin reintentar. GSG lee la URL y el secret vigentes de su base de datos en cada entrega (nunca viajan dentro de la cola) |
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 (los reenvíos usan la URL y el secret
actuales de tu webhook). Esos reenvíos llegan con el mismo
event_id original — otra razón por la que la deduplicación es obligatoria.
Mientras tanto, puedes reconciliar con la consulta por API.
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ó, rótalo (integración directa: desde tu panel; plataforma: pídelo a GSG).| 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. Verifica también que usas el secret vigente: cambia solo si lo rotaste (sección Webhook o al regenerar la API key marcando rotar el secret); regenerar la API key sin esa opción no cambia el secret. |
| Dejaste de recibir eventos | Tu endpoint respondió errores hasta agotar reintentos (o un 3xx) y los eventos quedaron retenidos, el webhook fue desactivado, o el vínculo de esa tienda dejó de estar activo (desvinculada, bloqueada o pendiente). Revisa GET /me → webhook y tu panel (integración directa) o contacta a GSG (plataforma) 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 8). |
| Pedidos duplicados o inconsistentes | Falta deduplicación por event_id. |
| Recibes eventos de tiendas que no reconoces | Plataforma multi-tienda: un negocio se vinculó con un external_store_id que tu sistema no conoce. Enrútalo por external_store_id/binding_id y responde 2xx igualmente. |
| GSG rechaza tu URL de webhook | Debe ser HTTPS, pública (no localhost ni IP privada), sin usuario:contraseña y ≤ 500 caracteres. |
| Versión | Fecha | Cambios |
|---|---|---|
| 1.2 | 2026-09-03 | Campos aditivos en data: state_id, route_type_code,
size_code, service. Nueva sección "Dónde se configura el webhook" con el escenario de
integración directa (el negocio gestiona su webhook desde el panel GSG). Referencias al
endpoint de consulta y a GET /me. Sin cambios en firma, cabeceras ni
event_version. Revisión del mismo día: una respuesta 3xx deja el evento retenido
de inmediato (sin reintentos; motivo Redirección … GSG no sigue redirecciones; registre la URL final);
eventos generados con el vínculo de la tienda no activo quedan retenidos y son recuperables al reactivarlo;
DRIVER_ASIGNADO/DRIVER_DESASIGNADO ahora generan evento; previous_status es
null (no "null") sin estado previo; route_state_id presente en todos los caminos;
el webhook_secret de integración directa se muestra al enviar la solicitud y solo cambia al rotarlo;
GET /me → webhook es null para plataformas. |
| 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.
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. |