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 %
Alle Artikel
WebhooksAlarmeTutorial

Gold- und Silberpreisalarme mit Webhooks automatisieren

Gold- und Silberpreisalarme mit signierten Webhooks automatisieren: Bedingungen, Sperrfristen, Payload, HMAC-SHA256-Prüfung in Node und Python sowie Wiederholungen.

Von MetalAPI Team··9 Min. Lesezeit

Alarme statt Polling

Viele Integrationen interessieren sich nur zu bestimmten Zeitpunkten für Preise: wenn Gold eine Marke überschreitet, nach der ein Kunde gefragt hat, wenn Silber stark fällt oder wenn sich eine Währung so weit bewegt, dass ein Katalog neu bepreist werden muss. Wer /latest einmal pro Minute abfragt, um diese Momente zu erwischen, verbraucht etwa 45.000 Anfragen im Monat und muss die Logik, die das Kreuzen der Schwelle erkennt, trotzdem selbst schreiben.

Preisalarme von MetalAPI verlagern diese Arbeit auf den Server. Die Alarm-Engine prüft jeden aktiven Alarm gegen die Live-Preise, sobald sie eintreffen, und benachrichtigt Sie, wenn eine Bedingung erfüllt ist. Alarmprüfungen verbrauchen kein API-Kontingent, und Ihr Code läuft nur, wenn tatsächlich etwas passiert.

TarifAlarmeZustellung
FreeKeine—
Lite3E-Mail
Standard10E-Mail
Advanced25E-Mail oder Webhook
Pro50E-Mail oder Webhook
Ultra100E-Mail oder Webhook
Business200E-Mail oder Webhook
Enterprise500E-Mail oder Webhook

Sie verwalten Alarme im Dashboard unter Alerts. Die Webhook-Anleitung ist die kurze Referenz; dieser Artikel behandelt die Details, die Sie für einen Endpunkt im Produktivbetrieb brauchen. Wenn Sie jede Preisaktualisierung benötigen statt nur Schwellenwert-Ereignisse, beschreibt der Artikel zum Echtzeit-Streaming den anderen Ansatz.

Alarmbedingungen und wann sie auslösen

Jeder Alarm überwacht ein Symbol, bepreist in einer Währung, zum Beispiel XAU in USD oder XAG in EUR. Jedes aktive Symbol ist möglich, einschließlich Platin, Palladium, Industriemetallen, Energie, Währungen und Krypto. Es gibt vier Bedingungen:

BedingungLöst aus, wennSchwellenwert
abovePreis ≥ SchwellenwertEin Preis in der Währung des Alarms
belowPreis ≤ SchwellenwertEin Preis in der Währung des Alarms
change_pct_upVeränderung seit dem vorherigen Schlusskurs ≥ SchwellenwertEin Prozentsatz unter 100
change_pct_downVeränderung seit dem vorherigen Schlusskurs ≤ −SchwellenwertEin Prozentsatz unter 100, als positive Zahl eingegeben

Schwellenwerte für Metalle gelten pro Feinunze, der kanonischen Einheit. Das gilt auch für Industriemetalle wie Kupfer, die anderswo oft pro Pfund notiert werden. Energie verwendet die jeweilige Kontrakteinheit, bei Rohöl ein Barrel, und der Webhook-Payload gibt die Einheit immer an.

Preise und prozentuale Veränderungen werden in der Währung des Alarms berechnet. Bei XAG in EUR kann allein ein stärkerer Euro Silber unter Ihren Schwellenwert drücken oder change_pct_down auslösen, selbst wenn Silber in US-Dollar unverändert ist.

Alarme lösen beim Kreuzen der Schwelle aus, nicht bei jeder Preisaktualisierung, solange die Bedingung erfüllt ist. Steigt Gold über 4.000 $, erhalten Sie eine Benachrichtigung. Der Alarm wird wieder scharf geschaltet, sobald die Bedingung zwischenzeitlich nicht mehr erfüllt war. Fällt der Preis also später unter die Marke und steigt erneut darüber, werden Sie wieder benachrichtigt.

Die Sperrfrist (Cooldown) ist die Mindestzeit zwischen zwei Benachrichtigungen desselben Alarms: zwischen 5 Minuten und 7 Tagen (10.080 Minuten), standardmäßig 60 Minuten. Ein Preis, der um einen Schwellenwert pendelt, kann ihn innerhalb einer Stunde viele Male kreuzen; die Sperrfrist macht daraus eine einzige Nachricht. Für eine Marke, die nur einmal wichtig ist, wählen Sie eine lange Sperrfrist und pausieren den Alarm, nachdem er ausgelöst hat.

Ein Verhalten ist beim Testen praktisch. Ein neuer Alarm hat keinen vorherigen Zustand. Ist seine Bedingung bereits erfüllt, löst er deshalb mit der nächsten Preisaktualisierung aus, bei laufendem Handel meist innerhalb einer Minute. Ein Alarm für XAU über 1 USD mit einer Sperrfrist von 5 Minuten liefert Ihnen eine echte Zustellung zum Testen. Löschen Sie ihn danach wieder.

Zustellung per E-Mail oder Webhook

E-Mail-Alarme sind ab dem Lite-Tarif verfügbar. Sie richten sich an Menschen: ein Betreff wie „MetalAPI alert: Gold (XAU) rose above 4000 USD“, der aktuelle Preis und die Notiz, die Sie dem Alarm mitgegeben haben.

Webhooks, verfügbar ab dem Advanced-Tarif, richten sich an Systeme: ein HTTPS-POST mit JSON-Body an eine URL Ihrer Wahl, signiert, damit Sie nachweisen können, dass er von MetalAPI stammt. Eine Webhook-URL muss:

  • https:// verwenden,
  • auf eine öffentliche IP-Adresse auflösen (private, Loopback- und Link-Local-Bereiche werden beim Speichern des Alarms abgelehnt und bei der Zustellung erneut geprüft),
  • ohne Benutzernamen und Passwort auskommen.

Weiterleitungen werden nicht verfolgt, geben Sie also die endgültige URL ein. Wechselt ein Konto in einen Tarif ohne Webhooks, werden seine Webhook-Alarme per E-Mail zugestellt statt verworfen.

Alle Webhooks eines Kontos werden mit einem einzigen Secret signiert, das mit whsec_ beginnt. Sie können es im Bereich Webhook signing secret auf der Alerts-Seite erstellen, anzeigen, kopieren und rotieren. Haben Sie noch keines erstellt, wird es bei der ersten Webhook-Zustellung erzeugt und danach dort angezeigt. Eine Rotation wirkt sofort, was für den Prüfcode weiter unten wichtig ist.

Die Webhook-Anfrage

Jede Zustellung sieht so aus:

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"
}
FeldBedeutung
eventDerzeit immer alert.triggered. Ignorieren Sie unbekannte Werte, damit neue Ereignistypen Ihren Handler nie aus dem Tritt bringen.
idNumerische Ereignis-ID, eindeutig pro ausgelöstem Alarm und bei Wiederholungen identisch
createdUnix-Zeit, zu der der Alarm ausgelöst hat
alertDer Alarm wie konfiguriert: id (eine UUID), symbol, base, condition, threshold und note
priceDer Preis, der den Alarm ausgelöst hat, in base pro unit
unittroy_oz für Metalle, bbl, mmbtu oder gal für Energie, currency für Währungen, coin für Krypto

Das Feld note mit bis zu 200 Zeichen eignet sich gut für Routing-Informationen, die Sie im Dashboard festlegen, etwa einen Teamnamen, einen Kanal oder eine Artikelnummer (SKU).

Signatur in Node.js prüfen

Der Header X-MetalAPI-Signature hat die Form t=<timestamp>,v1=<signature>. Die Signatur ist der HMAC-SHA256 der Zeichenkette <timestamp>.<raw body> als Hex in Kleinbuchstaben, berechnet mit Ihrem whsec_-Secret als Schlüssel. Der Zeitstempel wird zusätzlich separat in X-MetalAPI-Timestamp gesendet. Drei Details entscheiden darüber, ob die Prüfung im Produktivbetrieb funktioniert:

  1. Die Rohbytes hashen. Wenn Sie das JSON parsen und erneut serialisieren, können sich Leerzeichen oder Zahlenformate ändern, und die Signatur stimmt nicht mehr. Lesen Sie den Body als Buffer.
  2. Den Zeitstempel aus dem Header prüfen. Verwenden Sie t aus dem Header, nicht created aus dem Body. Jede Wiederholung wird beim Senden mit einem neuen t neu signiert, während created die ursprüngliche Zeit behält. Ein Replay-Fenster auf Basis von created würde gültige Wiederholungen ablehnen.
  3. Während einer Rotation zwei Secrets akzeptieren. Eine Rotation im Dashboard stellt die Signierung sofort auf das neue Secret um. Fügen Sie das neue Secret neben dem alten hinzu und deployen Sie; Zustellungen, die in der Zwischenzeit fehlschlagen, werden einige Minuten lang wiederholt. Entfernen Sie das alte Secret, sobald das neue funktioniert.

Mit Express liefert express.raw() den Body als Buffer:

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

// Kommagetrennt: das aktuelle Secret, während einer Rotation zusätzlich das vorherige.
const SECRETS = (process.env.METALAPI_WEBHOOK_SECRETS ?? "").split(",").filter(Boolean);
const TOLERANCE_S = 300; // Signaturen ablehnen, die älter als 5 Minuten sind

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); // innerhalb von 8 Sekunden antworten, dann die Arbeit erledigen
  if (event.event === "alert.triggered") {
    handleAlert(event).catch((err) => console.error("alert handler failed:", err));
  }
});

app.listen(3000);

Der Handler antwortet, bevor er irgendetwas erledigt. Ist die Arbeit wichtig, speichern Sie das Ereignis zuerst dauerhaft (wie, zeigen die nächsten Abschnitte), damit ein Absturz nach der Antwort es nicht verlieren kann.

Signatur in Python prüfen

Dieselbe Prüfung in Python kommt mit der Standardbibliothek aus. hmac.compare_digest sorgt für den Vergleich in konstanter Zeit:

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

from flask import Flask, abort, request

# Kommagetrennt: das aktuelle Secret, während einer Rotation zusätzlich das vorherige.
SECRETS = [s for s in os.environ.get("METALAPI_WEBHOOK_SECRETS", "").split(",") if s]
TOLERANCE_S = 300  # Signaturen ablehnen, die älter als 5 Minuten sind

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()  # Rohbytes, vor jedem JSON-Parsing
    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"]
        # Hier das Ereignis an eine Job-Queue übergeben und schnell antworten.
        app.logger.warning("%s/%s %s %s: %s", alert["symbol"], alert["base"],
                           alert["condition"], alert["threshold"], event["price"])
    return "", 204

Andere Frameworks unterscheiden sich nur darin, wie Sie den Roh-Body lesen: await request.body() in FastAPI und request.body in Django. Stellen Sie unabhängig vom Framework sicher, dass keine Middleware den Body geparst und verbraucht hat, bevor Ihr Handler läuft.

Wiederholungen, Duplikate und Antwortzeiten

Eine Zustellung ist erfolgreich, wenn Ihr Endpunkt innerhalb von 8 Sekunden mit einem beliebigen 2xx-Status antwortet. Timeouts, Verbindungsfehler, Weiterleitungen und alle anderen Statuscodes gelten als Fehlschlag und werden wiederholt:

VersuchZeitpunkt
1Sobald der Alarm auslöst
2Etwa 1 Minute nach dem Auslösen
3Etwa 4 Minuten nach dem Auslösen

Nach drei Versuchen wird das Ereignis als fehlgeschlagen markiert. Die Tabelle Recent deliveries auf der Alerts-Seite listet jedes Ereignis mit seinem Ergebnis auf, einschließlich HTTP-Status oder Fehlermeldung des letzten Versuchs. Dort sollten Sie zuerst nachsehen, wenn sich ein Endpunkt merkwürdig verhält.

Wiederholungen verwenden dieselbe Ereignis-id. Eine Zustellung, die Ihr Server zwar verarbeitet, aber erst nach mehr als 8 Sekunden beantwortet hat, gilt bei MetalAPI als fehlgeschlagen und kommt erneut an. Der Handler muss daher idempotent sein. Am einfachsten geht das mit einer Tabelle, deren Schlüssel die Ereignis-ID ist:

PostgreSQL
create table metalapi_events (
  event_id     bigint primary key,      -- "id" aus dem Payload, bei Wiederholungen identisch
  alert_id     uuid not null,
  payload      jsonb not null,
  received_at  timestamptz not null default now(),
  processed_at timestamptz
);

-- Im Webhook-Handler, nach der Signaturprüfung und vor der Antwort:
insert into metalapi_events (event_id, alert_id, payload)
values ($1, $2, $3)
on conflict (event_id) do nothing;

Fügen Sie das Ereignis ein, antworten Sie mit 204 und lassen Sie einen Worker die Zeilen verarbeiten, bei denen processed_at null ist. Eine doppelte Zustellung trifft auf den Primärschlüssel und ändert nichts. Das Insert dauert Millisekunden, sodass Sie selbst bei langsamer Folgearbeit deutlich innerhalb des 8-Sekunden-Fensters bleiben.

Alarme an Chat und andere Systeme weiterleiten

Ein häufiger Anwendungsfall ist das Posten von Alarmen in einen Team-Chat. Slack und Discord akzeptieren beide eingehende Webhooks, aber richten Sie einen MetalAPI-Alarm nicht direkt auf sie: Sie erwarten einen anderen Payload, und Sie würden die Signaturprüfung verlieren. Prüfen Sie auf Ihrem eigenen Endpunkt, formatieren Sie dann und leiten Sie weiter. Diese Version von handleAlert ersetzt den Logging-Platzhalter im Node.js-Server oben:

Weiterleitung an einen Chat-Webhook
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})` : "");

  // Eingehende Slack-Webhooks erwarten {"text": ...}; Discord-Webhooks erwarten {"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}`);
}

Für den Beispiel-Payload lautet die Nachricht „XAU rose above 4000 USD: now 4001.25 USD per troy_oz (Gold above 4k)“. Dasselbe Muster funktioniert mit allem, was eine HTTP-API hat: ein Ticket eröffnen, gecachte Produktpreise in Ihrem Shop aktualisieren, wenn sich Gold an einem Tag um mehr als 2 % bewegt, oder die Person benachrichtigen, die einen Preisfeed pflegt. Führen Sie die Weiterleitung in einem Worker aus statt im Request-Handler, damit ein langsamer Chat-Dienst eine Zustellung nie in einen Timeout laufen lässt.

Um Ihren Endpunkt zu testen, ohne auf den Markt zu warten, signieren Sie einen Beispiel-Payload selbst mit Ihrem Secret:

Signiertes Testereignis senden
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"

Ein 204 bedeutet, dass die Signaturprüfung bestanden ist. Für einen End-to-End-Test erstellen Sie, wie oben beschrieben, einen Alarm, dessen Bedingung bereits erfüllt ist, und beobachten, wie das Ereignis unter Recent deliveries erscheint. Alarme mit Webhook-Zustellung gibt es ab dem Advanced-Tarif; die Tarife finden Sie auf der Preisseite, oder erstellen Sie ein Konto, um zunächst E-Mail-Alarme in Lite auszuprobieren.

Probieren Sie es mit Ihren eigenen Daten aus

Erstellen Sie ein kostenloses Konto und senden Sie Ihre erste Anfrage in unter zwei Minuten. 100 Anfragen pro Monat, keine Kreditkarte.

Weiterlesen