Menú

Webhooks de alertas de precios

Recibe una solicitud POST firmada en tu URL cuando el oro, la plata o el cobre cruza un nivel, cambia un porcentaje o cierra el día. Añade una alerta en tu panel, verifica cada solicitud y luego procésala.

Plus · $39/moPro · $89/mo

Alertas y límites

Cada alerta vigila el precio al contado de un metal, oro, plata o cobre, con una regla y envía a una URL. Plus y Pro incluyen 5 alertas cada uno. Free y Basic no incluyen alertas de precios. Si tu plan termina o falla el pago, los envíos se detienen hasta que el plan vuelva a estar activo.

  • Cruce de nivel: el precio pasa de un lado de un nivel al otro, hacia arriba, hacia abajo o en ambas direcciones. Tras activarse, la alerta permanece en silencio durante al menos 10 minutos. Si el precio cruza de vuelta y regresa durante ese tiempo, no se envía el segundo cruce. Solo se rearma cuando ha pasado ese tiempo y el precio ha vuelto al lado del nivel donde estaba antes del cruce, rebasándolo en un 0.1%. Así se notifica un segundo cruce real y se filtran las pequeñas oscilaciones alrededor del nivel.
  • Cambio porcentual: el precio cambia un porcentaje en 1 hora o 24 horas. La alerta se activa una vez. Se rearma tras 1 hora, cuando el movimiento vuelve a estar por debajo del umbral. Un movimiento de 24 horas justo después de la apertura semanal del mercado se mide desde el precio de reapertura. La diferencia de precio del fin de semana nunca cuenta como movimiento.
  • Cierre diario: el último precio del día UTC, enviado justo después de la medianoche UTC. Si un metal no tiene precio ese día, como puede ocurrir en un fin de semana, no tiene cierre.

Los días se calculan en UTC. Las alertas de cierre diario se envían una vez al día. Si el precio oscila alrededor de un nivel, no se envía una solicitud por cada actualización de precio.

Eventos que puedes recibir

  • price.level_cross: el precio cruzó el nivel que configuraste.
  • price.percent_move: el precio cambió el porcentaje que configuraste.
  • price.daily_close: terminó el día UTC.
  • webhook.test: pulsaste Send test en una alerta. Su data es {"alert_id":"<alert id>"}. Puedes enviar una prueba por alerta por minuto y no puede haber más de 20 pruebas pendientes de entrega a la vez.

Leer la solicitud

Recibes un POST en tu URL. La URL debe usar https://, el puerto 443 y un nombre de servidor público, sin nombre de usuario ni contraseña y con un máximo de 2048 caracteres. La dirección se comprueba antes de cada entrega. No se siguen las redirecciones y una respuesta 3xx cuenta como fallo. Solo se lee tu código de estado.

Content-Type: application/jsonUser-Agent: webhook-delivery/1Webhook-Signature: t=<unix seconds>,v1=<hex>

El cuerpo es JSON con cuatro campos de nivel superior: id, type, created_at y data. El orden de las claves y los espacios del cuerpo real pueden diferir de los ejemplos de esta página.

idstring
Un identificador de evento único. Úsalo para omitir duplicados.
typestring
Uno de los tipos de evento anteriores.
created_atstring
Una fecha y hora UTC en formato ISO 8601 que termina en Z. Para un cierre diario, es 23:59:59 del día que cerró.
data.alert_idstring
La alerta que se activó. No cambia cuando rotas el secreto.
data.kindstring
level_cross, percent_move o daily_close.
data.symbolstring
XAU-USD-SPOT, XAG-USD-SPOT o HG-USD-SPOT.
data.directionstring
above, below o either, según lo configurado en la alerta.
data.pricestring
El precio que activó la alerta, como cadena decimal. Solo para eventos de nivel y porcentaje.
data.observed_atstring
El momento en que se observó el precio. Solo para eventos de nivel y porcentaje.
data.levelstring
El nivel que configuraste. Solo para eventos de nivel.
data.percent, data.window, data.reference_pricestring
El porcentaje que configuraste, su periodo (1h o 24h) y el precio al inicio del periodo. Solo para eventos de porcentaje.
data.date, data.closestring
El día UTC y su último precio. Solo para eventos de cierre diario.

Ejemplos de cuerpos

price.level_cross

{
  "id": "evt_3f0c6a52-6a2e-4b8e-9c1d-2f4b7e9a1c03_level_cross:492841",
  "type": "price.level_cross",
  "created_at": "2026-10-23T14:02:11.482113Z",
  "data": {
    "alert_id": "3f0c6a52-6a2e-4b8e-9c1d-2f4b7e9a1c03",
    "kind": "level_cross",
    "symbol": "XAU-USD-SPOT",
    "direction": "above",
    "price": "4203.1",
    "level": "4200",
    "observed_at": "2026-10-23T14:02:11.482113+00:00"
  }
}

price.percent_move

{
  "id": "evt_8a1d52c4-0b7e-4f55-a3d9-61e2c7b8f410_percent_move:492855",
  "type": "price.percent_move",
  "created_at": "2026-10-23T09:15:40.120934Z",
  "data": {
    "alert_id": "8a1d52c4-0b7e-4f55-a3d9-61e2c7b8f410",
    "kind": "percent_move",
    "symbol": "XAU-USD-SPOT",
    "direction": "either",
    "price": "4168.8",
    "percent": "2",
    "window": "24h",
    "reference_price": "4083.05",
    "observed_at": "2026-10-23T09:15:40.120934+00:00"
  }
}

price.daily_close

{
  "id": "evt_c92e0f7b-31a4-4d6a-8e57-a05b3d1c92e8_close:2026-10-22",
  "type": "price.daily_close",
  "created_at": "2026-10-22T23:59:59Z",
  "data": {
    "alert_id": "c92e0f7b-31a4-4d6a-8e57-a05b3d1c92e8",
    "kind": "daily_close",
    "symbol": "XAG-USD-SPOT",
    "direction": "either",
    "date": "2026-10-22",
    "close": "48.62"
  }
}

webhook.test

{
  "id": "evt_test_1792143012345",
  "type": "webhook.test",
  "created_at": "2026-10-23T09:30:12.345000Z",
  "data": {
    "alert_id": "3f0c6a52-6a2e-4b8e-9c1d-2f4b7e9a1c03"
  }
}

Verificar las firmas

Comprueba Webhook-Signature antes de actuar sobre un evento. Usa los octetos del cuerpo sin procesar, exactamente como los recibiste. Nunca analices y vuelvas a serializar el JSON antes de verificarlo.

  1. Divide Webhook-Signature por las comas para leer t y v1.
  2. Rechaza la solicitud si t está más de 300 segundos por delante o por detrás de tu reloj.
  3. Construye el mensaje con t, un punto y el cuerpo sin procesar: <t>. seguido del cuerpo.
  4. Calcula el HMAC-SHA256 del mensaje con el secreto de firma de tu alerta, que empieza por whsec_, y codifica el resultado en hexadecimal.
  5. Compáralo con v1 mediante una comparación de tiempo constante. Rechaza la solicitud si no coinciden.

Node.js

Python

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

const TOLERANCE_SECONDS = 300;

// rawBody: the request body exactly as received (a Buffer), before any JSON parsing.
function verifyWebhook(rawBody, header, secret, now = Math.floor(Date.now() / 1000)) {
  const fields = {};
  for (const part of String(header ?? "").split(",")) {
    const i = part.indexOf("=");
    if (i > 0) fields[part.slice(0, i).trim()] = part.slice(i + 1).trim();
  }
  const t = Number(fields.t);
  if (!Number.isInteger(t) || Math.abs(now - t) > TOLERANCE_SECONDS) return false;
  const expected = createHmac("sha256", secret).update(t + ".").update(rawBody).digest();
  const given = Buffer.from(fields.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

const seen = new Set(); // use your database in production

createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const rawBody = Buffer.concat(chunks);
    const header = req.headers["webhook-signature"];
    if (!verifyWebhook(rawBody, header, process.env.WEBHOOK_SECRET)) {
      res.writeHead(400).end();
      return;
    }
    const event = JSON.parse(rawBody);
    if (!seen.has(event.id)) {
      seen.add(event.id);
      // Hand the event to a queue or background job here, then answer quickly.
      console.log(event.type, event.data);
    }
    res.writeHead(204).end();
  });
}).listen(Number(process.env.PORT ?? 3000));
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_webhook(raw_body: bytes, header: str, secret: str, now: int | None = None) -> bool:
    """raw_body is the request body exactly as received, before any JSON parsing."""
    fields = dict(p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p)
    try:
        t = int(fields["t"])
    except (KeyError, ValueError):
        return False
    now = int(time.time()) if now is None else now
    if abs(now - t) > TOLERANCE_SECONDS:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(fields.get("v1", "").encode(), expected.encode())


# Flask:  verify_webhook(request.get_data(), request.headers.get("Webhook-Signature", ""), secret)
# Django: verify_webhook(request.body, request.headers.get("Webhook-Signature", ""), secret)

Responder y gestionar los reintentos

Devuelve cualquier 2xx en un máximo de 10 segundos. Un 3xx, 4xx, 5xx, un tiempo de espera agotado o un error de conexión cuenta como intento fallido. Guarda el evento y responde rápido. Haz el trabajo lento en segundo plano.

  • Primer reintento: 1 minuto después del primer fallo.
  • Segundo reintento: 5 minutos después del fallo anterior.
  • Tercer reintento: 30 minutos después del fallo anterior.
  • Cuarto reintento: 2 horas después del fallo anterior.
  • Quinto y último reintento: 6 horas después del fallo anterior.

Son 6 intentos durante unas 8.5 horas. Tras el último intento fallido, el evento no se vuelve a enviar y no hay una alternativa por correo electrónico. Cada intento se firma de nuevo con una marca de tiempo nueva. Se descarta cualquier evento que tenga más de una hora cuando se intenta enviar por primera vez. Los registros de entrega se conservan durante 30 días.

Rotar el secreto de firma

El secreto se muestra una sola vez, al añadir la alerta. Si sospechas que se ha filtrado o lo perdiste, elige Rotate secret en la alerta de tu panel. El nuevo secreto se muestra una sola vez. La alerta conserva su identificador, el secreto anterior deja de funcionar de inmediato y las entregas posteriores usan el nuevo. Para evitar entregas fallidas, permite que tu servidor acepte ambos secretos durante unos minutos mientras haces el cambio.

Preguntas

¿Qué plan necesito para las alertas de precios?

Plus ($39/mes) y Pro ($89/mes) incluyen 5 alertas de precios cada uno. Las alertas de precios no están incluidas ni en el plan Free ni en el plan Basic.

¿Con qué frecuencia se puede activar una alerta?

Tras activarse, una alerta de nivel permanece en silencio durante al menos 10 minutos; los cruces durante ese tiempo no se envían. Solo se rearma cuando ha pasado ese tiempo y el precio ha vuelto al lado del nivel donde estaba antes del cruce, rebasándolo en un 0.1%. La de porcentaje se activa una vez y se rearma tras 1 hora, cuando desaparece el movimiento. El cierre diario se envía una vez por día UTC.

¿Qué pasa si mi servidor no está disponible?

Una entrega fallida se reintenta hasta 5 veces más durante unas 8.5 horas. Tras el último intento fallido, el evento no se vuelve a enviar y no hay una alternativa por correo electrónico.

¿Puedo recibir el mismo evento dos veces?

Sí. Una entrega puede reintentarse aunque ya la hayas procesado, por ejemplo, si tu respuesta llega después de 10 segundos. Guarda los identificadores de los eventos procesados y omite los duplicados.

Consulta también