Автоматические оповещения о ценах на золото и серебро через вебхуки
Оповещения о ценах на золото и серебро через подписанные вебхуки: условия, период ожидания, полезная нагрузка, проверка HMAC-SHA256 на Node и Python, повторы.
Оповещения вместо опроса
Многим интеграциям цены важны лишь в определённые моменты: когда золото пересекает уровень, о котором спрашивал клиент, когда серебро резко дешевеет или когда валютный курс сдвигается настолько, что пора пересчитать цены в каталоге. Опрос /latest раз в минуту, чтобы не пропустить такие моменты, стоит около 45 000 запросов в месяц, и логику, которая обнаруживает пересечение, всё равно приходится писать самому.
Ценовые оповещения MetalAPI переносят эту работу на сервер. Механизм оповещений сверяет каждое активное оповещение с живыми ценами по мере их поступления и уведомляет вас, когда условие выполнено. Проверки оповещений не расходуют квоту API, а ваш код запускается только тогда, когда что-то происходит.
| Тариф | Оповещения | Доставка |
|---|---|---|
| Free | Нет | — |
| Lite | 3 | |
| Standard | 10 | |
| Advanced | 25 | Email или вебхук |
| Pro | 50 | Email или вебхук |
| Ultra | 100 | Email или вебхук |
| Business | 200 | Email или вебхук |
| Enterprise | 500 | Email или вебхук |
Оповещениями управляют в разделе Alerts личного кабинета. Руководство по вебхукам — краткий справочник, а в этой статье разобраны детали, которые нужны эндпоинту в продакшене. Если вам нужно каждое обновление цены, а не события пересечения порога, другой подход описан в статье о стриминге в реальном времени.
Условия оповещений и когда они срабатывают
Каждое оповещение отслеживает один символ в одной валюте, например XAU в USD или XAG в EUR. Подходит любой активный символ, включая платину, палладий, промышленные металлы, энергоносители, валюты и криптовалюты. Условий четыре:
| Условие | Срабатывает, когда | Порог |
|---|---|---|
above | цена ≥ порога | Цена в валюте оповещения |
below | цена ≤ порога | Цена в валюте оповещения |
change_pct_up | изменение с предыдущего закрытия ≥ порога | Процент меньше 100 |
change_pct_down | изменение с предыдущего закрытия ≤ −порог | Процент меньше 100, вводится положительным числом |
Пороги для металлов задаются за тройскую унцию — базовую единицу. Это касается и промышленных металлов, например меди, которую в других источниках часто котируют за фунт. Для энергоносителей используется единица контракта (для сырой нефти — баррель), а полезная нагрузка вебхука всегда указывает единицу измерения.
Цены и процентные изменения рассчитываются в валюте оповещения. Для XAG в EUR одно лишь укрепление евро может опустить серебро ниже вашего порога или вызвать срабатывание change_pct_down, даже если в долларах США цена серебра не изменилась.
Оповещения срабатывают в момент пересечения, а не при каждом обновлении цены, пока условие истинно. Когда золото поднимается выше 4 000 $, вы получаете одно уведомление. Оповещение снова активируется, как только условие становится ложным, поэтому если цена позже опустится ниже уровня, а затем снова его пересечёт, вы получите новое уведомление.
Период ожидания — минимальное время между двумя уведомлениями от одного оповещения: от 5 минут до 7 дней (10 080 минут), по умолчанию 60 минут. Цена, которая колеблется около порога, может пересечь его много раз за час, а период ожидания превращает это в одно сообщение. Если уровень важен только один раз, задайте длинный период ожидания и приостановите оповещение после срабатывания.
Одна особенность удобна для тестирования. У нового оповещения нет предыдущего состояния, поэтому если его условие уже истинно, оно срабатывает при следующем обновлении цены — обычно в течение минуты, пока идут торги. Оповещение для XAU выше 1 USD с периодом ожидания 5 минут даст вам реальную доставку для тестирования. Потом удалите его.
Доставка по email или через вебхук
Оповещения по электронной почте доступны начиная с тарифа Lite. Они предназначены для людей: тема вида «MetalAPI alert: Gold (XAU) rose above 4000 USD», текущая цена и заметка, которую вы добавили к оповещению.
Вебхуки, доступные начиная с тарифа Advanced, предназначены для систем: это HTTPS-запрос POST с телом в JSON на выбранный вами URL, подписанный так, чтобы вы могли убедиться, что он пришёл от MetalAPI. URL вебхука должен:
- использовать
https://, - указывать на публичный IP-адрес (частные, loopback- и link-local-диапазоны отклоняются при сохранении оповещения и повторно проверяются при доставке),
- не содержать имени пользователя и пароля.
Перенаправления не выполняются, поэтому указывайте конечный URL. Если аккаунт переходит на тариф без вебхуков, его оповещения с доставкой через вебхук приходят по электронной почте, а не теряются.
Все вебхуки аккаунта подписываются одним секретом, который начинается с whsec_. Создать, показать, скопировать и сменить его можно на панели Webhook signing secret на странице Alerts. Если вы его ещё не создали, он генерируется при первой доставке вебхука и затем отображается на панели. Смена секрета вступает в силу немедленно, и это важно для кода проверки ниже.
Запрос вебхука
Каждая доставка выглядит так:
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"
}| Поле | Значение |
|---|---|
event | Сейчас всегда alert.triggered. Игнорируйте неизвестные значения, чтобы новые типы событий никогда не ломали ваш обработчик. |
id | Числовой идентификатор события, уникальный для каждого срабатывания и одинаковый во всех повторных попытках |
created | Время Unix, когда сработало оповещение |
alert | Оповещение в том виде, в каком оно настроено: id (UUID), symbol, base, condition, threshold и note |
price | Цена, при которой сработало оповещение, в base за unit |
unit | troy_oz для металлов, bbl, mmbtu или gal для энергоносителей, currency для валют, coin для криптовалют |
Поле note длиной до 200 символов удобно для данных маршрутизации, которые вы задаёте в личном кабинете, например названия команды, канала или артикула товара.
Проверка подписи в Node.js
Заголовок X-MetalAPI-Signature имеет вид t=<timestamp>,v1=<signature>. Подпись — это HMAC-SHA256 в шестнадцатеричной записи строчными буквами от строки <timestamp>.<raw body>, вычисленный с вашим секретом whsec_ в качестве ключа. Метка времени также передаётся отдельно в X-MetalAPI-Timestamp. Будет ли проверка работать в продакшене, зависит от трёх деталей:
- Хешируйте необработанные байты. Разбор JSON и повторная сериализация могут изменить пробелы или формат чисел и сломать подпись. Читайте тело как буфер.
- Проверяйте метку времени из заголовка. Используйте
tиз заголовка, а неcreatedиз тела. Каждая повторная попытка заново подписывается в момент отправки с новымt, аcreatedсохраняет исходное время. Окно защиты от повторного воспроизведения на основеcreatedотклоняло бы допустимые повторные доставки. - Принимайте два секрета во время смены. При смене секрета в личном кабинете подпись сразу переключается на новый секрет. Добавьте новый секрет рядом со старым и разверните изменения; доставки, которые за это время завершатся ошибкой, будут повторены в течение нескольких минут. Удалите старый секрет, когда новый заработает.
В Express express.raw() передаёт тело как Buffer:
// webhook-server.mjs — Node.js 18+, npm install express
import crypto from "node:crypto";
import express from "express";
// Через запятую: текущий секрет, а во время смены — ещё и предыдущий.
const SECRETS = (process.env.METALAPI_WEBHOOK_SECRETS ?? "").split(",").filter(Boolean);
const TOLERANCE_S = 300; // отклонять подписи старше 5 минут
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 секунд, а затем выполняйте работу
if (event.event === "alert.triggered") {
handleAlert(event).catch((err) => console.error("alert handler failed:", err));
}
});
app.listen(3000);Обработчик отвечает до того, как выполнить работу. Если работа важна, сначала надёжно сохраните событие (как это сделать, показано в следующих разделах), чтобы сбой после ответа не привёл к его потере.
Проверка подписи в Python
Та же проверка на Python использует только стандартную библиотеку. hmac.compare_digest обеспечивает сравнение за постоянное время:
# webhook_app.py — pip install flask, затем: flask --app webhook_app run --port 3000
import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
# Через запятую: текущий секрет, а во время смены — ещё и предыдущий.
SECRETS = [s for s in os.environ.get("METALAPI_WEBHOOK_SECRETS", "").split(",") if s]
TOLERANCE_S = 300 # отклонять подписи старше 5 минут
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() # необработанные байты, до разбора 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"]
# Здесь передайте событие в очередь задач и быстро верните ответ.
app.logger.warning("%s/%s %s %s: %s", alert["symbol"], alert["base"],
alert["condition"], alert["threshold"], event["price"])
return "", 204Другие фреймворки отличаются лишь способом чтения необработанного тела: await request.body() в FastAPI и request.body в Django. Какой бы фреймворк вы ни использовали, убедитесь, что никакое middleware не разобрало и не прочитало тело до запуска вашего обработчика.
Повторные попытки, дубликаты и время ответа
Доставка считается успешной, если ваш эндпоинт отвечает любым статусом 2xx в течение 8 секунд. Тайм-ауты, ошибки соединения, перенаправления и все остальные статусы считаются неудачами, и доставка повторяется:
| Попытка | Когда |
|---|---|
| 1 | Сразу после срабатывания оповещения |
| 2 | Примерно через 1 минуту после срабатывания |
| 3 | Примерно через 4 минуты после срабатывания |
После трёх попыток событие помечается как неудачное. В таблице Recent deliveries на странице Alerts перечислены все события с результатами, включая HTTP-статус или сообщение об ошибке последней попытки. Именно туда стоит заглянуть в первую очередь, если эндпоинт работает неправильно.
Повторные попытки используют тот же id события. Доставка, которую ваш сервер обработал, но ответил на неё позже чем через 8 секунд, на стороне MetalAPI считается неудачной и приходит снова, поэтому обработчик должен быть идемпотентным. Проще всего завести таблицу с идентификатором события в качестве ключа:
create table metalapi_events (
event_id bigint primary key, -- "id" из полезной нагрузки, одинаковый во всех повторах
alert_id uuid not null,
payload jsonb not null,
received_at timestamptz not null default now(),
processed_at timestamptz
);
-- В обработчике вебхука: после проверки подписи и до отправки ответа:
insert into metalapi_events (event_id, alert_id, payload)
values ($1, $2, $3)
on conflict (event_id) do nothing;Вставьте событие, ответьте 204 и поручите фоновому обработчику строки, у которых processed_at равно null. Повторная доставка упрётся в первичный ключ и ничего не изменит. Вставка занимает миллисекунды, поэтому вы с запасом уложитесь в 8 секунд, даже если последующая работа выполняется медленно.
Пересылка оповещений в чаты и другие системы
Распространённый сценарий — публикация оповещений в командном чате. И Slack, и Discord принимают входящие вебхуки, но не направляйте на них оповещения MetalAPI напрямую: они ожидают другую полезную нагрузку, и вы потеряете проверку подписи. Проверяйте подпись на собственном эндпоинте, затем форматируйте сообщение и пересылайте его. Эта версия handleAlert заменяет заглушку с логированием в сервере на Node.js выше:
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 ожидают {"text": ...}, вебхуки Discord — {"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}`);
}Для примера полезной нагрузки выше сообщение будет таким: «XAU rose above 4000 USD: now 4001.25 USD per troy_oz (Gold above 4k)». Тот же подход работает с любой системой, у которой есть HTTP API: можно создать тикет, обновить кешированные цены товаров в магазине, когда золото за день изменится больше чем на 2%, или уведомить того, кто отвечает за ценовой фид. Выполняйте пересылку в фоновом обработчике, а не в обработчике запроса, чтобы медленный чат-сервис никогда не приводил к тайм-ауту доставки.
Чтобы протестировать эндпоинт, не дожидаясь движения рынка, подпишите пример полезной нагрузки своим секретом:
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 означает, что проверка подписи пройдена. Для сквозного теста создайте оповещение, условие которого уже истинно, как описано выше, и посмотрите, как событие появится в Recent deliveries. Оповещения с доставкой через вебхук доступны начиная с тарифа Advanced; все тарифы описаны на странице цен, а оповещения по email можно сначала опробовать на Lite — для этого создайте аккаунт.
Попробуйте на своих данных
Создайте бесплатный аккаунт и отправьте первый запрос меньше чем за две минуты. 100 запросов в месяц, без банковской карты.
Читайте также
API цен на золото: полное руководство для разработчиков
Как работать с API цен на золото: получить ключ, вызвать API из curl, JavaScript и Python, правильно читать курсы, работать с историей, ошибками, квотами и кэшем.
Как добавить на сайт виджет с актуальной ценой золота
Виджет с актуальной ценой золота для WordPress, Shopify или HTML: создайте его в личном кабинете MetalAPI, выберите символы, макет и тему и вставьте две строки.
Цены на золото по каратам: 24K, 22K, 18K и как их рассчитать
Что означают 24K, 22K и 18K, как с ними связаны пробы 916 и 750, как рассчитать цену грамма золота по каратам и как получить её через API цен на золото.