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. Sudataes{"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.
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_moveodaily_close.- data.symbolstring
XAU-USD-SPOT,XAG-USD-SPOToHG-USD-SPOT.- data.directionstring
above,belowoeither, 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 (
1ho24h) 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.
- Divide
Webhook-Signaturepor las comas para leertyv1. - Rechaza la solicitud si
testá más de 300 segundos por delante o por detrás de tu reloj. - Construye el mensaje con
t, un punto y el cuerpo sin procesar:<t>.seguido del cuerpo. - Calcula el HMAC-SHA256 del mensaje con el secreto de firma de tu alerta, que empieza por
whsec_, y codifica el resultado en hexadecimal. - Compáralo con
v1mediante 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
- /docs/stream : precios en vivo por WebSocket y SSE
- /pricing : planes y límites