Webhook 与价格提醒
当价格突破您设定的阈值时,接收带签名的 HTTP 回调,并使用 HMAC-SHA256 验证签名。本指南介绍提醒条件、负载格式、Node.js 与 PHP 验证代码以及重试机制。
提醒与 Webhook 的工作原理
当价格提醒触发时,MetalAPI 会发送 Webhook。每个提醒以您选择的货币监控一个品种:当价格升至阈值以上或跌至阈值以下,或较上一收盘价上涨或下跌一定百分比时触发。
提醒在条件被越过的那一刻触发:条件变为成立时您会收到一次通知,条件再次变为不成立后提醒会重新启用。冷却时间(默认 60 分钟,可在 5 分钟至 7 天之间配置)可避免价格在阈值附近徘徊时连续发送通知。
Lite 及以上套餐可通过电子邮件发送提醒;Advanced 及以上套餐还可通过 Webhook(向您的服务器发送 HTTPS POST 请求)发送提醒。
设置 Webhook
- 在控制台中打开 Alerts 并创建一个提醒。
- 选择品种、货币、条件和阈值。
- 选择 Webhook 投递方式并输入您的端点 URL。
- 从控制台复制您的 Webhook 签名密钥(以
whsec_开头),并添加到服务器的配置中。
Webhook URL 必须是公网主机上的 https:// 地址。包含凭据或解析到私有、内部 IP 地址的 URL 会被拒绝,并且不会跟随重定向。
负载格式
每次投递都是一个 JSON POST 请求,包含以下请求头和正文:
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=3b9f0c4e…a71d
{
"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"
}condition 的取值为 above、below、change_pct_up 或 change_pct_down 之一。price 是触发提醒的价格,单位为每 unit 多少 base。id 对每个事件唯一,可用于忽略重复投递。
验证签名
在处理请求之前,务必确认它确实来自 MetalAPI。X-MetalAPI-Signature 请求头的格式为 t=<unix timestamp>,v1=<signature>。签名是以您的签名密钥为密钥,对字符串 <timestamp>.<raw request body> 计算出的 HMAC-SHA256 值,采用十六进制编码。
- 在进行任何 JSON 解析之前,对原始请求体计算 HMAC。
- 使用恒定时间比较签名。
- 拒绝超过五分钟的时间戳,以阻止重放请求。
在 Node.js 中使用 Express:
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.METALAPI_WEBHOOK_SECRET;
app.post("/webhooks/metalapi", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-MetalAPI-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const body = req.body.toString("utf8");
const expected = crypto.createHmac("sha256", SECRET).update(`${parts.t}.${body}`).digest("hex");
const valid =
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!valid || !fresh) return res.status(400).send("invalid signature");
const event = JSON.parse(body);
console.log(event.alert.symbol, event.alert.condition, event.alert.threshold, event.price);
res.sendStatus(200);
});在 PHP 中验证
用纯 PHP 实现同样的检查:
<?php
$secret = getenv('METALAPI_WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_METALAPI_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - $timestamp) > 300) {
http_response_code(400);
exit('invalid signature');
}
$event = json_decode($payload, true);
// 在此处理 $event['alert'] 和 $event['price']。
http_response_code(200);响应与重试
请在 8 秒内返回任意 2xx 状态码。耗时的工作请放在响应之后进行(例如放入队列),因为超时和非 2xx 响应都会被视为投递失败。
失败的投递会以逐渐增加的间隔重试,在提醒触发后的第一个小时内最多共尝试三次。如果某次投递在我们这边超时、但在您那边已经处理成功,之后仍可能收到重试,因此请保存事件 id,并确保您的处理程序是幂等的。
相关指南
准备好开始构建了吗?
创建免费账户获取 API 密钥,或在 API 参考中浏览所有端点。