Administracion de plataforma
Webhooks salientes
Envía a tus otros sistemas un aviso firmado cada vez que se crea, paga, cambia de estatus o se reembolsa un pedido. Guía para configurarlos y para programar el receptor que verifica la firma.
Webhooks salientes
Administración de plataforma. En Configuración → Webhooks. Incluidos en el plan Control (y en A la medida).
Qué es
Un webhook es una URL de tu sistema (ERP, contabilidad, un flujo de mensajería) a la que STE Flux le envía un POST con un JSON cada vez que pasa algo con tus pedidos. Así tu sistema se entera al momento sin tener que preguntar.
Cada envío va firmado con un secreto que sólo conocen tu sistema y STE Flux, se reintenta si tu servidor no responde y queda en un registro de entregas que puedes consultar y reenviar.
Cómo configurarlo
- Entra a Configuración → Webhooks y presiona Nuevo webhook.
- Escribe la URL de tu receptor. Debe empezar con
https://y ser una dirección pública. - Marca los eventos que quieres recibir y guarda.
- Copia el secreto que aparece (empieza con
whsec_). Sólo se muestra esta vez. Guárdalo en tu servidor como variable de entorno. - Presiona Enviar prueba en la fila del webhook: llega un evento
webhook.testfirmado. Si tu receptor responde2xx, está listo.
Si pierdes el secreto, usa Rotar secreto: se genera uno nuevo y el anterior deja de firmar en ese momento.
Eventos
| Evento | Cuándo se envía |
|---|---|
order.created |
Se creó un pedido de la tienda en línea (checkout, canal o cotización convertida). |
order.paid |
Un pedido quedó cobrado. Se envía una sola vez por pedido. |
order.status_changed |
Un pedido cambió de estatus: desde la tienda o desde el panel (lista, tablero, escaneo, paquetería, pago confirmado). |
payment.refunded |
Se reembolsó el pago de un pedido a través del proveedor de pagos. |
webhook.test |
Sólo cuando presionas Enviar prueba. |
Todavía no se envían: ventas de mostrador (POS), existencias bajas, y pagos o reembolsos que el proveedor confirma directamente por su webhook en pedidos de invitado. Están en la siguiente versión del catálogo.
Formato del envío
{
"id": "evt_6f1c0a1e9b2d4c58a1f0c2d3e4f5a6b7",
"type": "order.status_changed",
"schemaVersion": 1,
"createdAt": "2026-10-06T18:21:04.512Z",
"tenantId": "mi-negocio",
"data": {
"order": {
"id": "66f1c2…",
"number": "ORD-2026-000123",
"status": "SHIPPED",
"channel": "ONLINE_STORE",
"currency": "MXN",
"subtotal": "1000.00",
"discount": "100.00",
"shipping": "99.00",
"tax": "144.00",
"total": "1143.00",
"createdAt": "2026-10-06T17:02:11Z",
"paidAt": "2026-10-06T17:05:40Z",
"customer": { "id": "u-123", "name": "Ana Pérez", "email": "ana@example.com" },
"items": [
{ "productId": "p-1", "sku": "ARM-01", "name": "Armazón", "quantity": 2, "unitPrice": "500.00", "total": "1000.00" }
]
},
"previousStatus": "PAID",
"status": "SHIPPED",
"reason": "Recolección de paquetería"
}
}
- Los importes viajan como texto decimal (
"1143.00") para que ningún lenguaje los redondee como flotante. order.createdyorder.paidtraen sólodata.order.order.status_changedagregapreviousStatus,statusyreason.previousStatuspuede venirnullsi el estatus anterior no quedó registrado.payment.refundedagregadata.refundconamount,currencyyreason.- No se envían teléfono, dirección ni datos fiscales del cliente.
Encabezados
| Encabezado | Contenido |
|---|---|
X-Flux-Signature |
t=<segundos unix>,v1=<firma hex> |
X-Flux-Event-Id |
Igual a id del cuerpo. Es estable: úsalo para no procesar dos veces el mismo evento. |
X-Flux-Event-Type |
Igual a type. |
X-Flux-Delivery-Id |
La entrega (un evento hacia un webhook). Se repite en los reintentos. |
X-Flux-Delivery-Attempt |
Número de intento, desde 1. |
User-Agent |
STE-Flux-Webhooks/1.0 |
Verificar la firma
La firma es HMAC-SHA256(secreto, "<t>.<cuerpo>") en hexadecimal, donde <t> es el valor t del encabezado y <cuerpo> son los bytes exactos que recibiste (no lo vuelvas a serializar). La llave es el secreto completo, incluido el prefijo whsec_.
Comprueba también que t no tenga más de 5 minutos de diferencia con tu reloj: así nadie puede repetir un envío capturado. Cada reintento se firma de nuevo con su propio t.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.FLUX_WEBHOOK_SECRET;
app.post('/flux/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Flux-Signature') ?? '';
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body}`).digest('hex');
const ok = parts.v1 && parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// Responde rápido y procesa después; ignora un event.id que ya procesaste.
res.sendStatus(204);
});
Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["FLUX_WEBHOOK_SECRET"].encode()
@app.post("/flux/webhooks")
def flux_webhook():
header = request.headers.get("X-Flux-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300:
abort(400)
body = request.get_data() # bytes exactos
expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = request.get_json()
return "", 204
Java
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = HexFormat.of().formatHex(mac.doFinal((t + "." + rawBody).getBytes(StandardCharsets.UTF_8)));
boolean valid = MessageDigest.isEqual(expected.getBytes(StandardCharsets.US_ASCII),
v1.getBytes(StandardCharsets.US_ASCII))
&& Math.abs(Instant.now().getEpochSecond() - t) <= 300;
Reintentos y entregas fallidas
- Una entrega es exitosa si tu servidor responde 2xx en menos de 10 segundos. Cualquier otra respuesta, un error de red o una redirección (no se siguen) cuenta como falla.
- Reintentamos hasta 10 veces con espera creciente: 30 s, 1 min, 2 min, 4 min, 8 min, 16 min, 32 min y luego cada hora (±20 % para no llegar todos juntos). El último reintento ocurre unas 3 horas después del evento.
- Agotados los intentos la entrega queda Fallida. Desde el Registro de entregas puedes ver cada intento (código y tiempo de respuesta) y Reenviar: se manda el mismo cuerpo con una firma nueva.
- Si 5 entregas seguidas terminan fallidas, desactivamos el webhook y te avisamos en la campana del panel. Cuando lo corrijas, vuelve a activarlo; el contador empieza de cero.
- El registro guarda las entregas 30 días.
Tu receptor puede recibir el mismo evento más de una vez (por ejemplo, si respondió tarde). Usa X-Flux-Event-Id para descartar duplicados.
Restricciones de seguridad
- Sólo
https://. No se aceptan usuario y contraseña dentro de la URL. - La URL debe resolver a una dirección pública. Se rechazan
localhost, redes privadas (10.x, 172.16–31.x, 192.168.x), direcciones de enlace local y de metadatos de nube (169.254.x), y nombres internos (.local,.internal,.svc). La comprobación se repite en cada envío, después de resolver el DNS. - Una entrega rechazada por esta regla no se reintenta y aparece como El destino no es una dirección pública.
- Se leen como máximo 64 KB de la respuesta y nunca se guarda su contenido.
Qué puede salir mal
| Situación | Qué pasa |
|---|---|
| Tu plan no es Control | La pantalla muestra la invitación a cambiar de plan y no se crean entregas. Si bajas de Control, las entregas pendientes se cierran sin enviarse. |
| Tu servidor responde 401/400 | Revisa que verifiques la firma sobre el cuerpo sin modificar y con el secreto vigente. |
| Rotaste el secreto | Los envíos se firman con el nuevo desde ese momento; actualiza tu receptor. |
| Tu servidor redirige (301/302) | No se sigue la redirección: registra la URL final. |
Permisos
Ver la pantalla y el registro pide notifications:read; crear, editar, rotar y eliminar pide notifications:admin; enviar pruebas y reenviar pide notifications:execute. Los roles de administración del negocio los tienen.