MetalAPI
XAU3997,89−1,54 %XAG53,936−1,51 %XPT1554,47−0,83 %XPD1047,03−0,44 %XCU6,5080+2,29 %WTI103,27+3,82 %BRENT111,70−1,30 %EUR1,120400−0,01 %GBP1,337500−0,01 %JPY0,0063761+0,14 %CHF1,213445−0,25 %TRY0,020125−0,12 %BTC80.329,75−0,55 %ETH2329,36−1,13 %
Todos los artículos
WebhooksAlertasTutorial

Automatiza alertas de precio del oro y la plata con webhooks

Alertas de precio del oro y la plata con webhooks firmados: condiciones, periodos de espera, el payload, verificación HMAC-SHA256 en Node y Python, y reintentos.

Por MetalAPI Team··9 min de lectura

Alertas en lugar de consultas periódicas

Muchas integraciones solo necesitan los precios en momentos concretos: cuando el oro cruza un nivel por el que preguntó un cliente, cuando la plata cae con fuerza o cuando una divisa se mueve lo suficiente como para cambiar los precios de un catálogo. Consultar /latest una vez por minuto para captar esos momentos cuesta unas 45.000 solicitudes al mes, y además tienes que escribir la lógica que detecta el cruce.

Las alertas de precio de MetalAPI trasladan ese trabajo al servidor. El motor de alertas compara cada alerta activa con los precios en tiempo real a medida que llegan y te avisa cuando se cumple una condición. Las comprobaciones de alertas no consumen tu cuota de la API, y tu código solo se ejecuta cuando ocurre algo.

PlanAlertasEntrega
FreeNinguna—
Lite3Correo electrónico
Standard10Correo electrónico
Advanced25Correo electrónico o webhook
Pro50Correo electrónico o webhook
Ultra100Correo electrónico o webhook
Business200Correo electrónico o webhook
Enterprise500Correo electrónico o webhook

Las alertas se gestionan en Alerts, en el panel. La guía de webhooks es la referencia breve; este artículo cubre los detalles que necesitas para un endpoint que funcione en producción. Si necesitas cada actualización de precio en lugar de eventos de umbral, el artículo sobre streaming en tiempo real explica el otro enfoque.

Condiciones de alerta y cuándo se disparan

Cada alerta vigila un símbolo cotizado en una divisa, por ejemplo XAU en USD o XAG en EUR. Sirve cualquier símbolo activo, incluidos el platino, el paladio, los metales industriales, la energía, las divisas y las criptomonedas. Hay cuatro condiciones:

CondiciónSe dispara cuandoUmbral
aboveprecio ≥ umbralUn precio en la divisa de la alerta
belowprecio ≤ umbralUn precio en la divisa de la alerta
change_pct_upvariación desde el cierre anterior ≥ umbralUn porcentaje inferior a 100
change_pct_downvariación desde el cierre anterior ≤ −umbralUn porcentaje inferior a 100, introducido como número positivo

Los umbrales de los metales son por onza troy, la unidad canónica. Eso incluye metales industriales como el cobre, que en otros sitios suelen cotizarse por libra. La energía usa la unidad de su contrato, un barril en el caso del petróleo crudo, y el payload del webhook siempre indica la unidad.

Los precios y las variaciones porcentuales se calculan en la divisa de la alerta. Para XAG en EUR, basta con que el euro se fortalezca para que la plata baje de tu umbral o se dispare change_pct_down, aunque la plata no haya cambiado en dólares estadounidenses.

Las alertas se disparan en el cruce, no en cada actualización de precio mientras la condición se cumple. Cuando el oro supera los 4000 USD, recibes un solo aviso. La alerta se rearma cuando la condición vuelve a dejar de cumplirse, así que si más tarde el precio baja de ese nivel y vuelve a superarlo, recibes un nuevo aviso.

El periodo de espera es el tiempo mínimo entre dos avisos de la misma alerta: entre 5 minutos y 7 días (10.080 minutos), con 60 minutos por defecto. Un precio que oscila alrededor de un umbral puede cruzarlo muchas veces en una hora, y el periodo de espera lo convierte en un único mensaje. Para un nivel que solo importa una vez, usa un periodo de espera largo y pausa la alerta después de que se dispare.

Hay un comportamiento muy útil para hacer pruebas. Una alerta nueva no tiene estado previo, así que si su condición ya se cumple, se dispara con la siguiente actualización de precio, normalmente en menos de un minuto mientras el mercado está abierto. Una alerta para XAU por encima de 1 USD con un periodo de espera de 5 minutos te da una entrega real con la que probar. Bórrala después.

Entrega por correo electrónico o webhook

Las alertas por correo electrónico están disponibles a partir del plan Lite. Están pensadas para personas: un asunto como “MetalAPI alert: Gold (XAU) rose above 4000 USD”, el precio actual y la nota que hayas añadido a la alerta.

Los webhooks, disponibles a partir del plan Advanced, están pensados para sistemas: un POST HTTPS con un cuerpo JSON a la URL que elijas, firmado para que puedas demostrar que procede de MetalAPI. Una URL de webhook debe:

  • usar https://,
  • resolverse en una dirección IP pública (los rangos privados, de loopback y link-local se rechazan al guardar la alerta y se vuelven a comprobar en el momento de la entrega),
  • no contener usuario ni contraseña.

No se siguen las redirecciones, así que introduce la URL final. Si una cuenta pasa a un plan sin webhooks, sus alertas por webhook se entregan por correo electrónico en lugar de descartarse.

Todos los webhooks de una cuenta se firman con un único secreto que empieza por whsec_. Puedes crearlo, mostrarlo, copiarlo y rotarlo en el panel Webhook signing secret de la página Alerts. Si no has creado ninguno, se genera con la primera entrega de webhook y después aparece en el panel. La rotación surte efecto de inmediato, algo importante para el código de verificación de más abajo.

La solicitud del webhook

Cada entrega tiene este aspecto:

http
POST /webhooks/metalapi HTTP/1.1
Content-Type: application/json
User-Agent: MetalAPI-Webhooks/1.0
X-MetalAPI-Timestamp: 1792765800
X-MetalAPI-Signature: t=1792765800,v1=5f0c1e…9a2b

{
  "event": "alert.triggered",
  "id": 18342,
  "created": 1792765800,
  "alert": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "symbol": "XAU",
    "base": "USD",
    "condition": "above",
    "threshold": 4000,
    "note": "Gold above 4k"
  },
  "price": 4001.25,
  "unit": "troy_oz"
}
CampoSignificado
eventPor ahora siempre alert.triggered. Ignora los valores desconocidos para que los nuevos tipos de evento nunca rompan tu manejador.
idID numérico del evento, único por cada alerta disparada e idéntico en todos los reintentos
createdHora Unix en la que se disparó la alerta
alertLa alerta tal como está configurada: id (un UUID), symbol, base, condition, threshold y note
priceEl precio que disparó la alerta, en base por unit
unittroy_oz para los metales, bbl, mmbtu o gal para la energía, currency para las divisas, coin para las criptomonedas

El campo note, de hasta 200 caracteres, es un lugar práctico para la información de enrutamiento que defines en el panel, como el nombre de un equipo, un canal o el SKU de un producto.

Verificar la firma en Node.js

La cabecera X-MetalAPI-Signature tiene la forma t=<timestamp>,v1=<signature>. La firma es el HMAC-SHA256 en hexadecimal y minúsculas de la cadena <timestamp>.<raw body>, con tu secreto whsec_ como clave. La marca de tiempo también se envía por separado en X-MetalAPI-Timestamp. Tres detalles deciden si una comprobación funciona en producción:

  1. Calcula el hash de los bytes sin procesar. Analizar el JSON y volver a serializarlo puede cambiar los espacios o el formato de los números y romper la firma. Lee el cuerpo como un buffer.
  2. Comprueba la marca de tiempo de la cabecera. Usa t de la cabecera, no created del cuerpo. Cada reintento se vuelve a firmar en el momento del envío con un t nuevo, mientras que created conserva la hora original. Una ventana anti-replay basada en created rechazaría reintentos válidos.
  3. Acepta dos secretos durante una rotación. Al rotar el secreto en el panel, la firma pasa a usar el nuevo de inmediato. Añade el nuevo secreto junto al antiguo y despliega; las entregas que fallen mientras tanto se reintentan durante unos minutos. Elimina el secreto antiguo cuando el nuevo funcione.

Con Express, express.raw() entrega el cuerpo como un Buffer:

webhook-server.mjs
// webhook-server.mjs — Node.js 18+, npm install express
import crypto from "node:crypto";
import express from "express";

// Separados por comas: el secreto actual y, durante una rotación, también el anterior.
const SECRETS = (process.env.METALAPI_WEBHOOK_SECRETS ?? "").split(",").filter(Boolean);
const TOLERANCE_S = 300; // rechaza firmas con más de 5 minutos de antigüedad

export function verifySignature(rawBody, header) {
  const parts = {};
  for (const item of (header ?? "").split(",")) {
    const i = item.indexOf("=");
    if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
  }
  const ts = parts.t ?? "";
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > TOLERANCE_S) return false;
  if (!/^[0-9a-f]{64}$/.test(parts.v1 ?? "")) return false;
  const given = Buffer.from(parts.v1, "hex");
  return SECRETS.some((secret) => {
    const expected = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest();
    return crypto.timingSafeEqual(expected, given);
  });
}

async function handleAlert(event) {
  const { alert, price, unit } = event;
  console.log(`${alert.symbol}/${alert.base} ${alert.condition} ${alert.threshold}: ${price} per ${unit}`);
}

const app = express();

app.post("/webhooks/metalapi", express.raw({ type: "application/json", limit: "64kb" }), (req, res) => {
  if (!Buffer.isBuffer(req.body) || !verifySignature(req.body, req.get("X-MetalAPI-Signature"))) {
    return res.status(401).send("invalid signature");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(204); // responde en menos de 8 segundos y después haz el trabajo
  if (event.event === "alert.triggered") {
    handleAlert(event).catch((err) => console.error("alert handler failed:", err));
  }
});

app.listen(3000);

El manejador responde antes de hacer ningún trabajo. Si el trabajo es importante, guarda antes el evento de forma duradera (las siguientes secciones muestran cómo), para que un fallo después de la respuesta no pueda hacerlo desaparecer.

Verificar la firma en Python

La misma comprobación en Python solo usa la biblioteca estándar. hmac.compare_digest ofrece la comparación en tiempo constante:

webhook_app.py
# webhook_app.py — pip install flask y después: flask --app webhook_app run --port 3000
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

# Separados por comas: el secreto actual y, durante una rotación, también el anterior.
SECRETS = [s for s in os.environ.get("METALAPI_WEBHOOK_SECRETS", "").split(",") if s]
TOLERANCE_S = 300  # rechaza firmas con más de 5 minutos de antigüedad

app = Flask(__name__)


def verify_signature(raw: bytes, header: str) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    ts, given = parts.get("t", ""), parts.get("v1", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > TOLERANCE_S:
        return False
    signed = ts.encode() + b"." + raw
    return any(
        hmac.compare_digest(hmac.new(s.encode(), signed, hashlib.sha256).hexdigest(), given)
        for s in SECRETS
    )


@app.post("/webhooks/metalapi")
def metalapi_webhook():
    raw = request.get_data()  # bytes sin procesar, antes de analizar el JSON
    if not verify_signature(raw, request.headers.get("X-MetalAPI-Signature", "")):
        abort(401)
    event = json.loads(raw)
    if event.get("event") == "alert.triggered":
        alert = event["alert"]
        # Pasa aquí el evento a una cola de trabajos y responde rápido.
        app.logger.warning("%s/%s %s %s: %s", alert["symbol"], alert["base"],
                           alert["condition"], alert["threshold"], event["price"])
    return "", 204

Otros frameworks solo se diferencian en cómo se lee el cuerpo sin procesar: await request.body() en FastAPI y request.body en Django. Sea cual sea el framework, asegúrate de que ningún middleware haya analizado y consumido el cuerpo antes de que se ejecute tu manejador.

Reintentos, duplicados y tiempos de respuesta

Una entrega se considera correcta cuando tu endpoint responde con cualquier estado 2xx en menos de 8 segundos. Los tiempos de espera agotados, los errores de conexión, las redirecciones y cualquier otro estado cuentan como fallos y se reintentan:

IntentoCuándo
1En cuanto se dispara la alerta
2Aproximadamente 1 minuto después de dispararse la alerta
3Aproximadamente 4 minutos después de dispararse la alerta

Tras tres intentos, el evento se marca como fallido. La tabla Recent deliveries de la página Alerts muestra cada evento con su resultado, incluido el estado HTTP o el mensaje de error del último intento. Es el primer sitio donde mirar cuando un endpoint no se comporta como debe.

Los reintentos reutilizan el mismo id de evento. Una entrega que tu servidor procesó pero a la que respondió en más de 8 segundos cuenta como fallida para MetalAPI y vuelve a llegar, así que el manejador debe ser idempotente. La forma más sencilla es una tabla con el ID del evento como clave:

PostgreSQL
create table metalapi_events (
  event_id     bigint primary key,      -- "id" del payload, idéntico en todos los reintentos
  alert_id     uuid not null,
  payload      jsonb not null,
  received_at  timestamptz not null default now(),
  processed_at timestamptz
);

-- En el manejador del webhook, tras verificar la firma y antes de responder:
insert into metalapi_events (event_id, alert_id, payload)
values ($1, $2, $3)
on conflict (event_id) do nothing;

Inserta el evento, responde con 204 y deja que un worker procese las filas cuyo processed_at sea nulo. Una entrega duplicada choca con la clave primaria y no cambia nada. La inserción tarda milisegundos, lo que te mantiene muy por debajo de la ventana de 8 segundos aunque el trabajo posterior sea lento.

Reenviar alertas al chat y a otros sistemas

Un uso habitual es publicar las alertas en el chat de un equipo. Slack y Discord aceptan webhooks entrantes, pero no apuntes una alerta de MetalAPI directamente a ellos: esperan un payload distinto y perderías la verificación de la firma. Verifica en tu propio endpoint y después da formato y reenvía. Esta versión de handleAlert sustituye al stub de registro del servidor Node.js anterior:

Reenviar a un webhook de chat
const VERBS = { above: "rose above", below: "fell below", change_pct_up: "is up", change_pct_down: "is down" };

async function handleAlert({ alert, price, unit }) {
  const level = alert.condition.startsWith("change_pct")
    ? `${alert.threshold}% since the previous close`
    : `${alert.threshold} ${alert.base}`;
  const per = unit && unit !== "currency" ? ` per ${unit}` : "";
  const text = `${alert.symbol} ${VERBS[alert.condition]} ${level}: now ${price} ${alert.base}${per}` +
    (alert.note ? ` (${alert.note})` : "");

  // Los incoming webhooks de Slack esperan {"text": ...}; los webhooks de Discord, {"content": ...}.
  const body = process.env.CHAT_KIND === "discord" ? { content: text } : { text };
  const res = await fetch(process.env.CHAT_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) throw new Error(`chat webhook returned ${res.status}`);
}

Con el payload de ejemplo, el mensaje dice “XAU rose above 4000 USD: now 4001.25 USD per troy_oz (Gold above 4k)”. El mismo patrón funciona con cualquier cosa que tenga una API HTTP: abrir un ticket, actualizar los precios de productos en caché de tu tienda cuando el oro se mueve más de un 2 % en un día o avisar a quien mantenga un feed de precios. Haz el reenvío en un worker y no en el manejador de la solicitud, para que un servicio de chat lento nunca pueda hacer que una entrega agote el tiempo de espera.

Para probar tu endpoint sin esperar al mercado, firma tú mismo un payload de ejemplo con tu secreto:

Enviar un evento de prueba firmado
SECRET="whsec_your_secret"
BODY='{"event":"alert.triggered","id":1,"created":1792765800,"alert":{"id":"00000000-0000-0000-0000-000000000000","symbol":"XAU","base":"USD","condition":"above","threshold":4000,"note":"test"},"price":4001.25,"unit":"troy_oz"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -i -X POST http://localhost:3000/webhooks/metalapi \
  -H "Content-Type: application/json" \
  -H "X-MetalAPI-Timestamp: $TS" \
  -H "X-MetalAPI-Signature: t=$TS,v1=$SIG" \
  --data-raw "$BODY"

Un 204 significa que la comprobación de la firma ha pasado. Para una prueba de extremo a extremo, crea una alerta cuya condición ya se cumpla, como se ha descrito antes, y observa cómo aparece el evento en Recent deliveries. Las alertas con entrega por webhook empiezan en el plan Advanced; consulta los precios para ver los planes, o crea una cuenta para probar primero las alertas por correo electrónico en Lite.

Pruébalo con tus propios datos

Crea una cuenta gratuita y haz tu primera solicitud en menos de dos minutos. 100 solicitudes al mes, sin tarjeta de crédito.

Sigue leyendo