Webhooks y alertas de precio
Recibe callbacks HTTP firmados cuando un precio cruza tu umbral y verifícalos con HMAC-SHA256: configuración, payload, verificación de firma y reintentos.
Cómo funcionan las alertas y los webhooks
MetalAPI envía webhooks cuando se dispara una alerta de precio. Una alerta vigila un símbolo en la divisa que elijas. Se activa cuando el precio sube por encima o baja por debajo de un umbral, o cuando sube o baja un porcentaje determinado desde el cierre anterior.
Las alertas se disparan en el cruce: recibes un aviso una sola vez cuando la condición pasa a cumplirse, y la alerta se rearma después de que la condición haya dejado de cumplirse. Un periodo de espera (60 minutos por defecto, configurable de 5 minutos a 7 días) evita una avalancha de notificaciones cuando un precio oscila alrededor del umbral.
Las alertas te avisan por correo electrónico en el plan Lite y superiores, o por webhook (un POST HTTPS a tu servidor) en el plan Advanced y superiores.
Configurar un webhook
- Abre Alerts en el panel y crea una alerta.
- Elige el símbolo, la divisa, la condición y el umbral.
- Elige la entrega por webhook e introduce la URL de tu endpoint.
- Copia el secreto de firma de tu webhook (empieza por
whsec_) desde el panel a la configuración de tu servidor.
Las URL de webhook deben usar https:// en un host público. Se rechazan las URL que contienen credenciales o que se resuelven en direcciones IP privadas o internas, y no se siguen las redirecciones.
El payload
Cada entrega es un POST JSON con estas cabeceras y este cuerpo:
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=3b9f0c4e…a71d
{
"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"
}condition es uno de above, below, change_pct_up o change_pct_down. price es el precio que disparó la alerta, en base por unit. id es único para cada evento, así que puedes usarlo para ignorar entregas duplicadas.
Verificar la firma
Comprueba siempre que una solicitud procede realmente de MetalAPI antes de actuar en consecuencia. La cabecera X-MetalAPI-Signature tiene la forma t=<unix timestamp>,v1=<signature>. La firma es el HMAC-SHA256 codificado en hexadecimal de la cadena <timestamp>.<raw request body>, con tu secreto de firma como clave.
- Calcula el HMAC sobre el cuerpo sin procesar, antes de analizar el JSON.
- Compara las firmas en tiempo constante.
- Rechaza las marcas de tiempo de más de cinco minutos de antigüedad para bloquear las solicitudes reenviadas (ataques de repetición).
En Node.js con Express:
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.METALAPI_WEBHOOK_SECRET;
app.post("/webhooks/metalapi", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-MetalAPI-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const body = req.body.toString("utf8");
const expected = crypto.createHmac("sha256", SECRET).update(`${parts.t}.${body}`).digest("hex");
const valid =
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!valid || !fresh) return res.status(400).send("invalid signature");
const event = JSON.parse(body);
console.log(event.alert.symbol, event.alert.condition, event.alert.threshold, event.price);
res.sendStatus(200);
});Verificación en PHP
La misma comprobación en PHP puro:
<?php
$secret = getenv('METALAPI_WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_METALAPI_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - $timestamp) > 300) {
http_response_code(400);
exit('invalid signature');
}
$event = json_decode($payload, true);
// Gestiona aquí $event['alert'] y $event['price'].
http_response_code(200);Respuesta y reintentos
Devuelve cualquier código de estado 2xx en un plazo de 8 segundos. Haz el trabajo lento después de responder, por ejemplo, en una cola, porque los tiempos de espera agotados y las respuestas que no son 2xx cuentan como entregas fallidas.
Las entregas fallidas se reintentan con intervalos crecientes, hasta tres intentos en total durante la primera hora después de que se disparara la alerta. Un reintento puede llegar después de una entrega que agotó el tiempo de espera por nuestra parte pero que tuvo éxito por la tuya, así que guarda el id del evento y haz que tu manejador sea idempotente.
Guías relacionadas
¿Listo para empezar a desarrollar?
Crea una cuenta gratuita para obtener tu clave API o explora todos los endpoints en la referencia de la API.