MetalAPI
XAU3.997,89−%1,54XAG53,936−%1,51XPT1.554,47−%0,83XPD1.047,03−%0,44XCU6,5080+%2,29WTI103,27+%3,82BRENT111,70−%1,30EUR1,120400−%0,01GBP1,337500−%0,01JPY0,0063761+%0,14CHF1,213445−%0,25TRY0,020125−%0,12BTC80.329,75−%0,55ETH2.329,36−%1,13
Tüm yazılar
Webhook'larAlarmlarEğitim

Webhook'larla Altın ve Gümüş Fiyat Alarmlarını Otomatikleştirin

İmzalı webhook'larla altın ve gümüş fiyat alarmları: koşullar, bekleme süreleri, payload, Node ve Python'da HMAC-SHA256 doğrulama ve yeniden denemeler.

Yazar: MetalAPI Team··9 dk okuma

Sorgulama yerine alarmlar

Birçok entegrasyon fiyatlarla yalnızca belirli anlarda ilgilenir: altın bir müşterinin sorduğu bir seviyeyi geçtiğinde, gümüş sert düştüğünde ya da bir para birimi bir kataloğu yeniden fiyatlandırmayı gerektirecek kadar hareket ettiğinde. Bu anları yakalamak için /latest endpoint'ini dakikada bir sorgulamak ayda yaklaşık 45.000 istek harcar ve eşiğin geçildiğini tespit eden mantığı yine de sizin yazmanız gerekir.

MetalAPI fiyat alarmları bu işi sunucuya taşır. Alarm motoru, her aktif alarmı gelen canlı fiyatlarla karşılaştırır ve bir koşul gerçekleştiğinde sizi bilgilendirir. Alarm kontrolleri API kotanızı kullanmaz ve kodunuz yalnızca bir şey olduğunda çalışır.

PlanAlarmlarGönderim
FreeYok—
Lite3E-posta
Standard10E-posta
Advanced25E-posta veya webhook
Pro50E-posta veya webhook
Ultra100E-posta veya webhook
Business200E-posta veya webhook
Enterprise500E-posta veya webhook

Alarmları paneldeki Alerts bölümünden yönetirsiniz. Webhook rehberi kısa bir başvuru kaynağıdır; bu yazı ise canlı ortamda çalışan bir endpoint için gereken ayrıntıları ele alıyor. Eşik olayları yerine her fiyat güncellemesine ihtiyacınız varsa gerçek zamanlı akış yazısı diğer yaklaşımı anlatıyor.

Alarm koşulları ve ne zaman tetiklendikleri

Her alarm, tek bir para birimi cinsinden fiyatlanan tek bir sembolü izler; örneğin USD cinsinden XAU veya EUR cinsinden XAG. Platin, paladyum, endüstriyel metaller, enerji, döviz ve kripto dahil tüm aktif semboller kullanılabilir. Dört koşul vardır:

KoşulNe zaman tetiklenirEşik
abovefiyat ≥ eşikAlarmın para birimi cinsinden bir fiyat
belowfiyat ≤ eşikAlarmın para birimi cinsinden bir fiyat
change_pct_upönceki kapanıştan bu yana değişim ≥ eşik100'ün altında bir yüzde
change_pct_downönceki kapanıştan bu yana değişim ≤ −eşikPozitif sayı olarak girilen, 100'ün altında bir yüzde

Metal eşikleri, standart birim olan troy ons başınadır. Bu, başka yerlerde genellikle libre (pound) başına fiyatlanan bakır gibi endüstriyel metaller için de geçerlidir. Enerji kendi kontrat birimini kullanır (ham petrol için varil) ve webhook payload'ı birimi her zaman belirtir.

Fiyatlar ve yüzde değişimler alarmın para birimi cinsinden hesaplanır. EUR cinsinden XAG için, gümüş ABD doları cinsinden hiç değişmese bile yalnızca euronun güçlenmesi gümüşü eşiğinizin altına itebilir veya change_pct_down koşulunu tetikleyebilir.

Alarmlar, koşulun geçerli olduğu her fiyat güncellemesinde değil, eşiğin geçildiği anda tetiklenir. Altın 4.000 $ seviyesini yukarı yönde geçtiğinde tek bir bildirim alırsınız. Koşul yeniden geçersiz hale geldiğinde alarm tekrar kurulur; böylece fiyatın daha sonra seviyenin altına inip ardından yeniden yukarı geçmesi sizi tekrar bilgilendirir.

Bekleme süresi (cooldown), aynı alarmdan gelen iki bildirim arasındaki minimum süredir: 5 dakika ile 7 gün (10.080 dakika) arasında ayarlanabilir, varsayılan değer 60 dakikadır. Bir eşiğin çevresinde dalgalanan bir fiyat, bir saat içinde eşiği defalarca geçebilir; bekleme süresi bunu tek bir mesaja dönüştürür. Yalnızca bir kez önemli olan bir seviye için uzun bir bekleme süresi kullanın ve alarm tetiklendikten sonra onu duraklatın.

Bir davranış test için kullanışlıdır. Yeni bir alarmın önceki bir durumu yoktur; bu nedenle koşulu zaten geçerliyse bir sonraki fiyat güncellemesinde, piyasa açıkken genellikle bir dakika içinde tetiklenir. 5 dakikalık bekleme süresine sahip, XAU 1 USD'nin üzerindeyken tetiklenen bir alarm, test edebileceğiniz gerçek bir gönderim sağlar. Sonrasında alarmı silin.

E-posta veya webhook ile gönderim

E-posta alarmları Lite planından itibaren kullanılabilir. İnsanlar için tasarlanmıştır: “MetalAPI alert: Gold (XAU) rose above 4000 USD” gibi bir konu satırı, güncel fiyat ve alarma eklediğiniz not.

Advanced planından itibaren kullanılabilen webhook'lar ise sistemler için tasarlanmıştır: seçtiğiniz bir URL'ye JSON gövdeli bir HTTPS POST isteği; MetalAPI'den geldiğini kanıtlayabilmeniz için imzalanmıştır. Bir webhook URL'si:

  • https:// kullanmalı,
  • herkese açık bir IP adresine çözümlenmeli (özel, loopback ve link-local aralıklar alarmı kaydettiğinizde reddedilir ve gönderim sırasında yeniden kontrol edilir),
  • kullanıcı adı veya parola içermemelidir.

Yönlendirmeler takip edilmez, bu nedenle nihai URL'yi girin. Bir hesap webhook içermeyen bir plana geçerse webhook alarmları düşürülmek yerine e-posta ile gönderilir.

Bir hesabın tüm webhook'ları, whsec_ ile başlayan tek bir gizli anahtarla imzalanır. Bu anahtarı Alerts sayfasındaki Webhook signing secret panelinde oluşturabilir, görüntüleyebilir, kopyalayabilir ve yenileyebilirsiniz. Henüz oluşturmadıysanız ilk webhook gönderimiyle birlikte üretilir ve ardından panelde gösterilir. Yenileme hemen geçerli olur; bu da aşağıdaki doğrulama kodu açısından önemlidir.

Webhook isteği

Her gönderim şöyle görünür:

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"
}
AlanAnlamı
eventŞu anda her zaman alert.triggered. Yeni olay türlerinin işleyicinizi hiçbir zaman bozmaması için bilinmeyen değerleri yok sayın.
idSayısal olay kimliği; tetiklenen her alarm için benzersizdir ve yeniden denemelerde aynı kalır
createdAlarmın tetiklendiği Unix zamanı
alertYapılandırıldığı haliyle alarm: id (bir UUID), symbol, base, condition, threshold ve note
priceAlarmı tetikleyen fiyat; unit başına base cinsinden
unitMetaller için troy_oz, enerji için bbl, mmbtu veya gal, döviz için currency, kripto için coin

En fazla 200 karakter alan note alanı, panelde belirlediğiniz yönlendirme bilgileri için kullanışlı bir yerdir; örneğin bir ekip adı, bir kanal veya bir ürün SKU'su.

Node.js'te imzayı doğrulama

X-MetalAPI-Signature header'ı t=<timestamp>,v1=<signature> biçimindedir. İmza, <timestamp>.<raw body> dizesinin whsec_ gizli anahtarınızla hesaplanan, küçük harfli hex HMAC-SHA256 değeridir. Zaman damgası ayrıca tek başına X-MetalAPI-Timestamp içinde de gönderilir. Bir kontrolün canlı ortamda çalışıp çalışmayacağını üç ayrıntı belirler:

  1. Ham baytların hash'ini alın. JSON'u ayrıştırıp yeniden serileştirmek boşlukları veya sayı biçimlendirmesini değiştirip imzayı bozabilir. Gövdeyi bir buffer olarak okuyun.
  2. Header'daki zaman damgasını kontrol edin. Gövdedeki created değerini değil, header'daki t değerini kullanın. Her yeniden deneme, gönderim anında yeni bir t ile yeniden imzalanır; created ise ilk zamanı korur. created değerine dayalı bir tekrar oynatma (replay) penceresi geçerli yeniden denemeleri reddeder.
  3. Yenileme sırasında iki gizli anahtarı kabul edin. Panelde yenileme yapmak imzalamayı hemen yeni anahtara geçirir. Yeni anahtarı eskisinin yanına ekleyip dağıtın; bu arada başarısız olan gönderimler birkaç dakika boyunca yeniden denenir. Yeni anahtar çalıştığında eskisini kaldırın.

Express ile express.raw(), gövdeyi bir Buffer olarak sağlar:

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

// Virgülle ayrılmış: geçerli gizli anahtar ve yenileme sırasında bir öncekisi.
const SECRETS = (process.env.METALAPI_WEBHOOK_SECRETS ?? "").split(",").filter(Boolean);
const TOLERANCE_S = 300; // 5 dakikadan eski imzaları reddet

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); // 8 saniye içinde yanıt ver, işi sonra yap
  if (event.event === "alert.triggered") {
    handleAlert(event).catch((err) => console.error("alert handler failed:", err));
  }
});

app.listen(3000);

İşleyici, herhangi bir iş yapmadan önce yanıt verir. İş önemliyse olayı önce kalıcı olarak saklayın (sonraki bölümler nasıl yapılacağını gösteriyor); böylece yanıttan sonra yaşanan bir çökme olayın kaybolmasına yol açmaz.

Python'da imzayı doğrulama

Python'daki aynı kontrol yalnızca standart kütüphaneyi kullanır. Sabit zamanlı (constant-time) karşılaştırmayı hmac.compare_digest sağlar:

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

from flask import Flask, abort, request

# Virgülle ayrılmış: geçerli gizli anahtar ve yenileme sırasında bir öncekisi.
SECRETS = [s for s in os.environ.get("METALAPI_WEBHOOK_SECRETS", "").split(",") if s]
TOLERANCE_S = 300  # 5 dakikadan eski imzaları reddet

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()  # ham baytlar, herhangi bir JSON ayrıştırmasından önce
    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"]
        # Olayı burada bir iş kuyruğuna verin ve hızlıca dönün.
        app.logger.warning("%s/%s %s %s: %s", alert["symbol"], alert["base"],
                           alert["condition"], alert["threshold"], event["price"])
    return "", 204

Diğer framework'ler yalnızca ham gövdeyi nasıl okuduğunuz konusunda farklılık gösterir: FastAPI'de await request.body(), Django'da request.body. Framework ne olursa olsun, işleyiciniz çalışmadan önce hiçbir middleware'in gövdeyi ayrıştırıp tüketmediğinden emin olun.

Yeniden denemeler, yinelenen gönderimler ve yanıt süreleri

Endpoint'iniz 8 saniye içinde herhangi bir 2xx durum koduyla yanıt verdiğinde gönderim başarılı sayılır. Zaman aşımları, bağlantı hataları, yönlendirmeler ve diğer tüm durum kodları başarısızlık sayılır ve yeniden denenir:

DenemeNe zaman
1Alarm tetiklenir tetiklenmez
2Alarm tetiklendikten yaklaşık 1 dakika sonra
3Alarm tetiklendikten yaklaşık 4 dakika sonra

Üç denemeden sonra olay başarısız olarak işaretlenir. Alerts sayfasındaki Recent deliveries tablosu, son denemenin HTTP durum kodu veya hata mesajı dahil her olayı sonucuyla birlikte listeler. Bir endpoint beklendiği gibi çalışmadığında ilk bakılacak yer burasıdır.

Yeniden denemeler aynı olay id değerini kullanır. Sunucunuzun işlediği ancak 8 saniyeden uzun sürede yanıt verdiği bir gönderim MetalAPI tarafında başarısız sayılır ve yeniden gelir; bu nedenle işleyicinin idempotent olması gerekir. En basit yol, olay kimliğiyle anahtarlanmış bir tablodur:

PostgreSQL
create table metalapi_events (
  event_id     bigint primary key,      -- payload'daki "id", yeniden denemelerde aynıdır
  alert_id     uuid not null,
  payload      jsonb not null,
  received_at  timestamptz not null default now(),
  processed_at timestamptz
);

-- Webhook işleyicisinde, imzayı doğruladıktan sonra ve yanıt vermeden önce:
insert into metalapi_events (event_id, alert_id, payload)
values ($1, $2, $3)
on conflict (event_id) do nothing;

Olayı ekleyin, 204 ile yanıt verin ve processed_at değeri null olan satırları bir worker'ın işlemesine bırakın. Yinelenen bir gönderim birincil anahtara takılır ve hiçbir şeyi değiştirmez. Ekleme işlemi milisaniyeler sürer; bu da devamındaki iş yavaş olsa bile sizi 8 saniyelik pencerenin rahatça içinde tutar.

Alarmları sohbet uygulamalarına ve diğer sistemlere iletmek

Yaygın kullanımlardan biri, alarmları bir ekip sohbetine göndermektir. Slack ve Discord'un ikisi de gelen webhook'ları (incoming webhooks) kabul eder, ancak bir MetalAPI alarmını doğrudan onlara yönlendirmeyin: farklı bir payload beklerler ve imza doğrulamasını kaybedersiniz. Doğrulamayı kendi endpoint'inizde yapın, ardından mesajı biçimlendirip iletin. handleAlert fonksiyonunun bu sürümü, yukarıdaki Node.js sunucusundaki basit loglama fonksiyonunun yerini alır:

Bir sohbet webhook'una iletme
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})` : "");

  // Slack gelen webhook'ları {"text": ...}, Discord webhook'ları {"content": ...} bekler.
  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}`);
}

Örnek payload için mesaj şöyle olur: “XAU rose above 4000 USD: now 4001.25 USD per troy_oz (Gold above 4k)”. Aynı düzen HTTP API'si olan her şey için işe yarar: bir destek kaydı açın, altın bir günde %2'den fazla hareket ettiğinde mağazanızdaki önbelleğe alınmış ürün fiyatlarını yenileyin ya da bir fiyat akışının sorumlusunu bilgilendirin. Yavaş bir sohbet servisinin bir gönderimin zaman aşımına uğramasına asla yol açmaması için iletme işlemini istek işleyicisinde değil, bir worker'da çalıştırın.

Endpoint'inizi piyasayı beklemeden test etmek için örnek bir payload'ı kendi gizli anahtarınızla kendiniz imzalayın:

İmzalı bir test olayı gönderin
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"

204 yanıtı, imza kontrolünün geçtiği anlamına gelir. Uçtan uca bir test için yukarıda anlatıldığı gibi koşulu zaten geçerli olan bir alarm oluşturun ve olayın Recent deliveries tablosunda görünmesini izleyin. Webhook ile gönderilen alarmlar Advanced planından itibaren sunulur; planlar için fiyatlandırma sayfasına bakın ya da önce Lite planda e-posta alarmlarını denemek için bir hesap oluşturun.

Kendi verilerinizle deneyin

Ücretsiz bir hesap oluşturun ve ilk isteğinizi iki dakikadan kısa sürede gönderin. Ayda 100 istek, kredi kartı gerekmez.

Okumaya devam edin