Automatize alertas de preço do ouro e da prata com webhooks
Alertas de preço do ouro e da prata com webhooks assinados: condições, tempo de espera, payload, verificação HMAC-SHA256 em Node e Python e novas tentativas.
Alertas em vez de polling
Muitas integrações só se importam com os preços em momentos específicos: quando o ouro cruza um nível sobre o qual um cliente perguntou, quando a prata cai bruscamente ou quando uma moeda se move o suficiente para exigir a reprecificação de um catálogo. Fazer polling de /latest uma vez por minuto para captar esses momentos custa cerca de 45.000 requisições por mês, e você ainda precisa escrever a lógica que detecta o cruzamento.
Os alertas de preço da MetalAPI transferem esse trabalho para o servidor. O mecanismo de alertas confere cada alerta ativo com os preços ao vivo à medida que eles chegam e notifica você quando uma condição é atendida. As verificações de alertas não consomem a sua cota da API, e o seu código só roda quando algo acontece.
| Plano | Alertas | Entrega |
|---|---|---|
| Free | Nenhum | — |
| Lite | 3 | |
| Standard | 10 | |
| Advanced | 25 | E-mail ou webhook |
| Pro | 50 | E-mail ou webhook |
| Ultra | 100 | E-mail ou webhook |
| Business | 200 | E-mail ou webhook |
| Enterprise | 500 | E-mail ou webhook |
Você gerencia os alertas em Alerts no painel. O guia de webhooks é a referência rápida; este artigo cobre os detalhes de que você precisa para um endpoint que roda em produção. Se você precisa de cada atualização de preço, e não de eventos de limiar, o artigo sobre streaming em tempo real cobre a outra abordagem.
Condições de alerta e quando elas disparam
Cada alerta monitora um símbolo cotado em uma moeda, por exemplo XAU em USD ou XAG em EUR. Qualquer símbolo ativo funciona, incluindo platina, paládio, metais industriais, energia, moedas e cripto. Há quatro condições:
| Condição | Dispara quando | Limiar |
|---|---|---|
above | preço ≥ limiar | Um preço na moeda do alerta |
below | preço ≤ limiar | Um preço na moeda do alerta |
change_pct_up | variação desde o fechamento anterior ≥ limiar | Uma porcentagem abaixo de 100 |
change_pct_down | variação desde o fechamento anterior ≤ −limiar | Uma porcentagem abaixo de 100, informada como número positivo |
Os limiares dos metais são por onça troy, a unidade canônica. Isso inclui metais industriais como o cobre, que em outros lugares costuma ser cotado por libra-peso. A energia usa a unidade do contrato, o barril no caso do petróleo bruto, e o payload do webhook sempre informa a unidade.
Os preços e as variações percentuais são calculados na moeda do alerta. Para XAG em EUR, só o fortalecimento do euro já pode empurrar a prata abaixo do seu limiar ou disparar change_pct_down, mesmo que a prata não tenha mudado em dólares americanos.
Os alertas disparam no cruzamento, e não a cada atualização de preço enquanto a condição for verdadeira. Quando o ouro passa de US$ 4.000, você recebe uma notificação. O alerta é rearmado assim que a condição volta a ser falsa, então uma queda posterior abaixo do nível, seguida de uma nova alta acima dele, notifica você novamente.
O tempo de espera é o intervalo mínimo entre duas notificações do mesmo alerta: de 5 minutos a 7 dias (10.080 minutos), com padrão de 60 minutos. Um preço que oscila em torno de um limiar pode cruzá-lo muitas vezes em uma hora, e o tempo de espera transforma isso em uma única mensagem. Para um nível que só importa uma vez, use um tempo de espera longo e pause o alerta depois que ele disparar.
Um comportamento é útil para testes. Um alerta novo não tem estado anterior, então, se a condição já for verdadeira, ele dispara na próxima atualização de preço, geralmente em menos de um minuto enquanto o mercado estiver aberto. Um alerta para XAU acima de 1 USD com tempo de espera de 5 minutos fornece uma entrega real para você testar. Exclua-o depois.
Entrega por e-mail ou webhook
Os alertas por e-mail estão disponíveis a partir do plano Lite. Eles são feitos para pessoas: um assunto como “MetalAPI alert: Gold (XAU) rose above 4000 USD”, o preço atual e a observação que você anexou ao alerta.
Os webhooks, disponíveis a partir do plano Advanced, são feitos para sistemas: um POST HTTPS com um corpo JSON para uma URL de sua escolha, assinado para que você possa comprovar que ele veio da MetalAPI. Uma URL de webhook deve:
- usar
https://, - resolver para um endereço IP público (faixas privadas, de loopback e link-local são rejeitadas quando você salva o alerta e verificadas novamente no momento da entrega),
- não conter nome de usuário nem senha.
Redirecionamentos não são seguidos, então informe a URL final. Se uma conta passar para um plano sem webhooks, os seus alertas por webhook passam a ser entregues por e-mail em vez de serem descartados.
Todos os webhooks de uma conta são assinados com um único segredo que começa com whsec_. Você pode criá-lo, exibi-lo, copiá-lo e rotacioná-lo na seção Webhook signing secret da página Alerts. Se você ainda não tiver criado um, ele é gerado na primeira entrega de webhook e passa a aparecer nessa seção. A rotação tem efeito imediato, o que é importante para o código de verificação abaixo.
A requisição do webhook
Toda entrega tem esta aparência:
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"
}| Campo | Significado |
|---|---|
event | Atualmente sempre alert.triggered. Ignore valores desconhecidos para que novos tipos de evento nunca quebrem o seu handler. |
id | ID numérico do evento, único por alerta disparado e idêntico em todas as novas tentativas |
created | Horário Unix em que o alerta disparou |
alert | O alerta como foi configurado: id (um UUID), symbol, base, condition, threshold e note |
price | O preço que disparou o alerta, em base por unit |
unit | troy_oz para metais; bbl, mmbtu ou gal para energia; currency para moedas; coin para cripto |
O campo note, com até 200 caracteres, é um lugar prático para informações de roteamento que você define no painel, como o nome de uma equipe, um canal ou o SKU de um produto.
Verificando a assinatura no Node.js
O cabeçalho X-MetalAPI-Signature tem o formato t=<timestamp>,v1=<signature>. A assinatura é o HMAC-SHA256, em hexadecimal minúsculo, da string <timestamp>.<raw body>, usando o seu segredo whsec_ como chave. O timestamp também é enviado separadamente em X-MetalAPI-Timestamp. Três detalhes decidem se a verificação funciona em produção:
- Calcule o hash dos bytes brutos. Fazer o parsing do JSON e serializá-lo de novo pode alterar espaços em branco ou a formatação dos números e quebrar a assinatura. Leia o corpo como um buffer.
- Verifique o timestamp do cabeçalho. Use o
tdo cabeçalho, e não ocreateddo corpo. Cada nova tentativa é assinada de novo no momento do envio, com um novot, enquantocreatedmantém o horário original. Uma janela contra replay baseada emcreatedrejeitaria novas tentativas válidas. - Aceite dois segredos durante uma rotação. Rotacionar no painel passa a assinatura para o novo segredo na hora. Adicione o novo segredo ao lado do antigo e faça o deploy; as entregas que falharem nesse meio-tempo são repetidas por alguns minutos. Remova o segredo antigo quando o novo estiver funcionando.
Com o Express, express.raw() fornece o corpo como um Buffer:
// webhook-server.mjs — Node.js 18+, npm install express
import crypto from "node:crypto";
import express from "express";
// Separados por vírgula: o segredo atual e, durante uma rotação, também o anterior.
const SECRETS = (process.env.METALAPI_WEBHOOK_SECRETS ?? "").split(",").filter(Boolean);
const TOLERANCE_S = 300; // rejeita assinaturas com mais de 5 minutos
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); // responda em até 8 segundos e só depois faça o trabalho
if (event.event === "alert.triggered") {
handleAlert(event).catch((err) => console.error("alert handler failed:", err));
}
});
app.listen(3000);O handler responde antes de fazer qualquer trabalho. Se o trabalho for importante, armazene o evento de forma durável primeiro (as próximas seções mostram como), para que uma falha depois da resposta não o faça se perder.
Verificando a assinatura em Python
A mesma verificação em Python usa apenas a biblioteca padrão. hmac.compare_digest faz a comparação em tempo constante:
# webhook_app.py — pip install flask; depois: 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 vírgula: o segredo atual e, durante uma rotação, também o anterior.
SECRETS = [s for s in os.environ.get("METALAPI_WEBHOOK_SECRETS", "").split(",") if s]
TOLERANCE_S = 300 # rejeita assinaturas com mais de 5 minutos
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 brutos, antes de qualquer parsing de 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"]
# Entregue o evento a uma fila de jobs aqui e retorne rápido.
app.logger.warning("%s/%s %s %s: %s", alert["symbol"], alert["base"],
alert["condition"], alert["threshold"], event["price"])
return "", 204Outros frameworks só diferem na forma de ler o corpo bruto: await request.body() no FastAPI e request.body no Django. Seja qual for o framework, garanta que nenhum middleware tenha feito o parsing do corpo e o consumido antes de o seu handler rodar.
Novas tentativas, duplicatas e tempos de resposta
Uma entrega é bem-sucedida quando o seu endpoint responde com qualquer status 2xx em até 8 segundos. Timeouts, erros de conexão, redirecionamentos e qualquer outro status contam como falhas e são repetidos:
| Tentativa | Quando |
|---|---|
| 1 | Assim que o alerta dispara |
| 2 | Cerca de 1 minuto depois do disparo do alerta |
| 3 | Cerca de 4 minutos depois do disparo do alerta |
Depois de três tentativas, o evento é marcado como falho. A tabela Recent deliveries da página Alerts lista cada evento com o seu resultado, incluindo o status HTTP ou a mensagem de erro da última tentativa. É o primeiro lugar para olhar quando um endpoint se comporta mal.
As novas tentativas reutilizam o mesmo id de evento. Uma entrega que o seu servidor processou, mas respondeu depois de mais de 8 segundos, conta como falha do lado da MetalAPI e chega de novo, então o handler precisa ser idempotente. A forma mais simples é uma tabela com o ID do evento como chave:
create table metalapi_events (
event_id bigint primary key, -- "id" do payload, idêntico em todas as novas tentativas
alert_id uuid not null,
payload jsonb not null,
received_at timestamptz not null default now(),
processed_at timestamptz
);
-- No handler do webhook, depois de verificar a assinatura e antes de responder:
insert into metalapi_events (event_id, alert_id, payload)
values ($1, $2, $3)
on conflict (event_id) do nothing;Insira o evento, responda com 204 e deixe um worker processar as linhas em que processed_at é nulo. Uma entrega duplicada esbarra na chave primária e não altera nada. O insert leva milissegundos, o que mantém você bem dentro da janela de 8 segundos, mesmo quando o trabalho seguinte é lento.
Encaminhando alertas para o chat e outros sistemas
Um uso comum é publicar os alertas no chat da equipe. O Slack e o Discord aceitam incoming webhooks, mas não aponte um alerta da MetalAPI diretamente para eles: eles esperam outro payload, e você perderia a verificação da assinatura. Verifique no seu próprio endpoint e depois formate e encaminhe. Esta versão de handleAlert substitui o stub de log do servidor Node.js acima:
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})` : "");
// Os incoming webhooks do Slack esperam {"text": ...}; os webhooks do Discord esperam {"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}`);
}Para o payload de exemplo, a mensagem fica “XAU rose above 4000 USD: now 4001.25 USD per troy_oz (Gold above 4k)”. O mesmo padrão funciona com qualquer coisa que tenha uma API HTTP: abrir um ticket, atualizar os preços de produtos em cache na sua loja quando o ouro se move mais de 2% em um dia ou notificar quem mantém um feed de preços. Execute o encaminhamento em um worker, e não no handler da requisição, para que um serviço de chat lento nunca faça uma entrega expirar.
Para testar o seu endpoint sem esperar pelo mercado, assine você mesmo um payload de exemplo com o seu segredo:
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"Um 204 significa que a verificação da assinatura passou. Para um teste de ponta a ponta, crie um alerta cuja condição já seja verdadeira, como descrito acima, e veja o evento aparecer em Recent deliveries. Os alertas com entrega por webhook começam no plano Advanced; veja os planos em preços ou crie uma conta para experimentar primeiro os alertas por e-mail no Lite.
Experimente com seus próprios dados
Crie uma conta grátis e faça sua primeira requisição em menos de dois minutos. 100 requisições por mês, sem cartão de crédito.
Continue lendo
API de preços do ouro: o guia completo para desenvolvedores
Como usar uma API de preços do ouro: obtenha uma chave, chame-a com curl, JavaScript e Python, leia as cotações certo e trate histórico, erros, cotas e cache.
Como adicionar um widget de cotação do ouro ao vivo ao seu site
Widget com a cotação do ouro ao vivo no WordPress, na Shopify ou em HTML puro: crie no painel da MetalAPI, escolha símbolos, layout e tema e cole duas linhas.
Preços do ouro por quilate: 24K, 22K, 18K e como calculá-los
O que significam 24K, 22K e 18K, como se relacionam com teores como 916 e 750, como calcular o preço por grama de cada quilate e como obtê-lo por uma API.