MetalAPI
XAU3 997,89−1,54 %XAG53,936−1,51 %XPT1 554,47−0,83 %XPD1 047,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 %ETH2 329,36−1,13 %
Tous les articles
WebhooksAlertesTutoriel

Automatiser les alertes de prix de l'or et de l'argent avec des webhooks

Automatisez vos alertes de prix de l'or et de l'argent par webhooks signés : conditions, délai de carence, payload, vérification HMAC-SHA256 en Node et Python.

Par MetalAPI Team··9 min de lecture

Des alertes plutôt que du polling

Beaucoup d'intégrations ne s'intéressent aux prix qu'à des moments précis : quand l'or franchit un niveau surveillé par un client, quand l'argent chute brutalement, ou quand une devise bouge assez pour justifier de revoir les prix d'un catalogue. Interroger /latest une fois par minute pour saisir ces moments coûte environ 45 000 requêtes par mois, et il vous reste encore à écrire la logique qui détecte le franchissement.

Les alertes de prix de MetalAPI déplacent ce travail côté serveur. Le moteur d'alertes compare chaque alerte active aux prix en direct dès leur arrivée et vous prévient lorsqu'une condition est remplie. Les vérifications d'alertes ne consomment pas votre quota d'API, et votre code ne s'exécute que lorsqu'il se passe quelque chose.

PlanAlertesLivraison
FreeAucune—
Lite3E-mail
Standard10E-mail
Advanced25E-mail ou webhook
Pro50E-mail ou webhook
Ultra100E-mail ou webhook
Business200E-mail ou webhook
Enterprise500E-mail ou webhook

Vous gérez les alertes sous Alerts dans le tableau de bord. Le guide des webhooks sert de référence rapide ; cet article couvre les détails nécessaires pour un endpoint en production. Si vous avez besoin de chaque mise à jour de prix plutôt que d'événements de seuil, l'article sur le streaming en temps réel présente l'autre approche.

Conditions d'alerte et déclenchement

Chaque alerte surveille un symbole exprimé dans une devise, par exemple XAU en USD ou XAG en EUR. Tout symbole actif convient, y compris le platine, le palladium, les métaux industriels, l'énergie, les devises et les cryptos. Il existe quatre conditions :

ConditionSe déclenche lorsqueSeuil
aboveprix ≥ seuilUn prix dans la devise de l'alerte
belowprix ≤ seuilUn prix dans la devise de l'alerte
change_pct_upvariation depuis la clôture précédente ≥ seuilUn pourcentage inférieur à 100
change_pct_downvariation depuis la clôture précédente ≤ −seuilUn pourcentage inférieur à 100, saisi sous forme de nombre positif

Les seuils des métaux s'entendent à l'once troy, l'unité de référence. Cela vaut aussi pour les métaux industriels comme le cuivre, souvent cotés à la livre ailleurs. L'énergie utilise l'unité de son contrat, le baril pour le pétrole brut, et le payload du webhook indique toujours l'unité.

Les prix et les variations en pourcentage sont calculés dans la devise de l'alerte. Pour XAG en EUR, une hausse de l'euro peut à elle seule faire passer l'argent sous votre seuil ou déclencher change_pct_down, même si l'argent est inchangé en dollars américains.

Les alertes se déclenchent au franchissement, et non à chaque mise à jour de prix tant que la condition reste vraie. Lorsque l'or dépasse 4 000 $, vous recevez une seule notification. L'alerte se réarme une fois que la condition est redevenue fausse : un repli ultérieur sous ce niveau, suivi d'un nouveau franchissement à la hausse, vous notifie donc à nouveau.

Le délai de carence est le temps minimal entre deux notifications d'une même alerte : de 5 minutes à 7 jours (10 080 minutes), 60 minutes par défaut. Un prix qui oscille autour d'un seuil peut le franchir de nombreuses fois en une heure, et le délai de carence transforme tout cela en un seul message. Pour un niveau qui ne compte qu'une fois, choisissez un long délai de carence et mettez l'alerte en pause après son déclenchement.

Un comportement s'avère pratique pour les tests. Une nouvelle alerte n'a pas d'état antérieur : si sa condition est déjà vraie, elle se déclenche à la prochaine mise à jour de prix, généralement en moins d'une minute pendant les heures de marché. Une alerte sur XAU au-dessus de 1 USD avec un délai de carence de 5 minutes vous donne une vraie livraison sur laquelle tester. Supprimez-la ensuite.

Livraison par e-mail ou par webhook

Les alertes par e-mail sont disponibles à partir du plan Lite. Elles s'adressent à des personnes : un objet comme « MetalAPI alert: Gold (XAU) rose above 4000 USD », le prix actuel et la note que vous avez associée à l'alerte.

Les webhooks, disponibles à partir du plan Advanced, s'adressent à des systèmes : un POST HTTPS avec un corps JSON vers l'URL de votre choix, signé pour que vous puissiez prouver qu'il provient de MetalAPI. Une URL de webhook doit :

  • utiliser https://,
  • pointer vers une adresse IP publique (les plages privées, de bouclage et link-local sont rejetées à l'enregistrement de l'alerte, puis vérifiées à nouveau au moment de la livraison),
  • ne contenir ni nom d'utilisateur ni mot de passe.

Les redirections ne sont pas suivies : saisissez donc l'URL finale. Si un compte passe à un plan sans webhooks, ses alertes webhook sont livrées par e-mail au lieu d'être abandonnées.

Tous les webhooks d'un compte sont signés avec un seul secret qui commence par whsec_. Vous pouvez le créer, l'afficher, le copier et le renouveler dans le panneau Webhook signing secret de la page Alerts. Si vous n'en avez pas créé, il est généré lors de la première livraison de webhook, puis affiché dans le panneau. Le renouvellement prend effet immédiatement, ce qui a son importance pour le code de vérification ci-dessous.

La requête du webhook

Chaque livraison ressemble à ceci :

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"
}
ChampSignification
eventActuellement toujours alert.triggered. Ignorez les valeurs inconnues pour que de nouveaux types d'événements ne cassent jamais votre handler.
idIdentifiant numérique de l'événement, unique pour chaque alerte déclenchée et identique d'une relance à l'autre
createdHeure Unix à laquelle l'alerte s'est déclenchée
alertL'alerte telle que configurée : id (un UUID), symbol, base, condition, threshold et note
priceLe prix qui a déclenché l'alerte, en base par unit
unittroy_oz pour les métaux, bbl, mmbtu ou gal pour l'énergie, currency pour les devises, coin pour les cryptos

Le champ note, jusqu'à 200 caractères, est un endroit pratique pour les informations de routage que vous définissez dans le tableau de bord, comme un nom d'équipe, un canal ou une référence produit (SKU).

Vérifier la signature en Node.js

L'en-tête X-MetalAPI-Signature a la forme t=<timestamp>,v1=<signature>. La signature est le HMAC-SHA256, en hexadécimal minuscule, de la chaîne <timestamp>.<raw body>, calculé avec votre secret whsec_ comme clé. L'horodatage est aussi envoyé seul dans X-MetalAPI-Timestamp. Trois détails déterminent si une vérification fonctionne en production :

  1. Hachez les octets bruts. Analyser le JSON puis le resérialiser peut modifier les espaces ou le format des nombres et casser la signature. Lisez le corps sous forme de buffer.
  2. Vérifiez l'horodatage de l'en-tête. Utilisez t de l'en-tête, et non created du corps. Chaque relance est signée à nouveau au moment de l'envoi avec un nouveau t, tandis que created conserve l'heure d'origine. Une fenêtre anti-rejeu fondée sur created rejetterait des relances valides.
  3. Acceptez deux secrets pendant un renouvellement. Le renouvellement dans le tableau de bord bascule immédiatement la signature sur le nouveau secret. Ajoutez le nouveau secret à côté de l'ancien et déployez ; les livraisons qui échouent entre-temps sont relancées pendant quelques minutes. Retirez l'ancien secret une fois que le nouveau fonctionne.

Avec Express, express.raw() fournit le corps sous forme de Buffer :

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

// Séparés par des virgules : le secret actuel, plus le précédent pendant un renouvellement.
const SECRETS = (process.env.METALAPI_WEBHOOK_SECRETS ?? "").split(",").filter(Boolean);
const TOLERANCE_S = 300; // rejeter les signatures de plus de 5 minutes

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); // répondre en moins de 8 secondes, puis faire le travail
  if (event.event === "alert.triggered") {
    handleAlert(event).catch((err) => console.error("alert handler failed:", err));
  }
});

app.listen(3000);

Le handler répond avant d'effectuer le moindre travail. Si ce travail est important, stockez d'abord l'événement de façon durable (les sections suivantes montrent comment), afin qu'un plantage après la réponse ne puisse pas le perdre.

Vérifier la signature en Python

La même vérification en Python n'utilise que la bibliothèque standard. hmac.compare_digest assure la comparaison en temps constant :

webhook_app.py
# webhook_app.py — pip install flask, puis : flask --app webhook_app run --port 3000
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

# Séparés par des virgules : le secret actuel, plus le précédent pendant un renouvellement.
SECRETS = [s for s in os.environ.get("METALAPI_WEBHOOK_SECRETS", "").split(",") if s]
TOLERANCE_S = 300  # rejeter les signatures de plus de 5 minutes

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()  # octets bruts, avant toute analyse 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"]
        # Confiez ici l'événement à une file de tâches et répondez rapidement.
        app.logger.warning("%s/%s %s %s: %s", alert["symbol"], alert["base"],
                           alert["condition"], alert["threshold"], event["price"])
    return "", 204

Les autres frameworks ne diffèrent que par la façon de lire le corps brut : await request.body() dans FastAPI et request.body dans Django. Quel que soit le framework, assurez-vous qu'aucun middleware n'a analysé et consommé le corps avant l'exécution de votre handler.

Relances, doublons et temps de réponse

Une livraison réussit lorsque votre endpoint répond avec un statut 2xx quelconque en moins de 8 secondes. Les délais dépassés, les erreurs de connexion, les redirections et tous les autres statuts comptent comme des échecs et donnent lieu à des relances :

TentativeMoment
1Dès le déclenchement de l'alerte
2Environ 1 minute après le déclenchement de l'alerte
3Environ 4 minutes après le déclenchement de l'alerte

Après trois tentatives, l'événement est marqué comme échoué. Le tableau Recent deliveries de la page Alerts liste chaque événement avec son résultat, y compris le statut HTTP ou le message d'erreur de la dernière tentative. C'est le premier endroit à consulter lorsqu'un endpoint se comporte mal.

Les relances réutilisent le même id d'événement. Une livraison que votre serveur a traitée mais à laquelle il a répondu au-delà de 8 secondes compte comme un échec du côté de MetalAPI et arrive de nouveau : le handler doit donc être idempotent. Le plus simple est une table indexée par l'identifiant de l'événement :

PostgreSQL
create table metalapi_events (
  event_id     bigint primary key,      -- "id" du payload, identique d'une relance à l'autre
  alert_id     uuid not null,
  payload      jsonb not null,
  received_at  timestamptz not null default now(),
  processed_at timestamptz
);

-- Dans le handler du webhook, après la vérification de la signature et avant de répondre :
insert into metalapi_events (event_id, alert_id, payload)
values ($1, $2, $3)
on conflict (event_id) do nothing;

Insérez l'événement, répondez 204, et laissez un worker traiter les lignes dont processed_at est nul. Une livraison en double se heurte à la clé primaire et ne change rien. L'insertion prend quelques millisecondes, ce qui vous laisse largement dans la fenêtre de 8 secondes, même lorsque le travail qui suit est lent.

Transférer les alertes vers une messagerie et d'autres systèmes

Un usage courant consiste à publier les alertes dans la messagerie d'une équipe. Slack et Discord acceptent tous deux les webhooks entrants, mais n'y faites pas pointer directement une alerte MetalAPI : ils attendent un autre payload, et vous perdriez la vérification de signature. Vérifiez sur votre propre endpoint, puis mettez en forme et transférez. Cette version de handleAlert remplace la fonction de journalisation provisoire du serveur Node.js ci-dessus :

Transférer vers un webhook de messagerie
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})` : "");

  // Les webhooks entrants de Slack attendent {"text": ...} ; ceux de Discord attendent {"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}`);
}

Pour l'exemple de payload, le message est « XAU rose above 4000 USD: now 4001.25 USD per troy_oz (Gold above 4k) ». Le même principe fonctionne avec tout ce qui dispose d'une API HTTP : ouvrir un ticket, rafraîchir les prix des produits mis en cache dans votre boutique lorsque l'or bouge de plus de 2 % en une journée, ou prévenir la personne qui gère un flux de prix. Exécutez le transfert dans un worker plutôt que dans le handler de la requête, afin qu'un service de messagerie lent ne puisse jamais faire expirer une livraison.

Pour tester votre endpoint sans attendre le marché, signez vous-même un exemple de payload avec votre secret :

Envoyer un événement de test signé
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 signifie que la vérification de signature a réussi. Pour un test de bout en bout, créez une alerte dont la condition est déjà vraie, comme décrit plus haut, et regardez l'événement apparaître dans Recent deliveries. Les alertes livrées par webhook commencent au plan Advanced ; consultez les tarifs pour connaître les plans, ou créez un compte pour essayer d'abord les alertes par e-mail sur Lite.

Essayez avec vos propres données

Créez un compte gratuit et envoyez votre première requête en moins de deux minutes. 100 requêtes par mois, sans carte bancaire.

À lire aussi