Saltar al contenido
Centro de ayuda
Centro de ayuda

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

  1. Entra a Configuración → Webhooks y presiona Nuevo webhook.
  2. Escribe la URL de tu receptor. Debe empezar con https:// y ser una dirección pública.
  3. Marca los eventos que quieres recibir y guarda.
  4. Copia el secreto que aparece (empieza con whsec_). Sólo se muestra esta vez. Guárdalo en tu servidor como variable de entorno.
  5. Presiona Enviar prueba en la fila del webhook: llega un evento webhook.test firmado. Si tu receptor responde 2xx, 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.created y order.paid traen sólo data.order.
  • order.status_changed agrega previousStatus, status y reason. previousStatus puede venir null si el estatus anterior no quedó registrado.
  • payment.refunded agrega data.refund con amount, currency y reason.
  • 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.

También te puede ayudar