提醒与限制
每个提醒按一条规则监测一种金属的现货价格,黄金、白银或铜,并发送到一个URL。Plus和Pro各包含5个提醒。Free和Basic不包含价格提醒。如果套餐到期或付款失败,发送会暂停,直到套餐重新生效。
- 跨越价位:价格从指定价位的一侧移动到另一侧,可设为向上、向下或双向。触发后,至少10分钟内不再通知。如果价格在此期间反向跨过价位后又返回,第二次跨越不会发送。只有该时间已过,且价格已反向跨过价位,回到触发前所在的一侧并距该价位达到0.1%,提醒才会重新就绪。这样既能通知真正的再次跨越,也能过滤价位附近的小幅波动。
- 百分比变动:价格在1小时或24小时内变动指定百分比。触发一次后,需经过1小时,且变动幅度已回落到阈值以下,提醒才可再次触发。每周开市后不久的24小时变动以重新开市时的价格为基准。周末的价格跳空绝不会计入变动。
- 每日收盘:UTC当天的最后一个价格,在UTC午夜刚过时发送。如果某种金属当天没有价格,例如周末,则没有收盘价。
日期以UTC为准。每日收盘提醒每天发送一次。价格在指定价位附近波动时,不会每次价格更新都发送请求。
可接收的事件
price.level_cross:价格已跨过你设置的价位。price.percent_move:价格已按你设置的百分比变动。price.daily_close:UTC当天已结束。webhook.test:你点击了某个提醒的Send test。其data为{"alert_id":"<alert id>"}。每个提醒每分钟可发送一次测试,同时最多可有20个测试等待发送。
读取请求
你的URL会收到POST请求。URL必须使用https://、端口443和公共主机名,不得包含用户名或密码,长度不得超过2048个字符。每次发送前都会检查地址。不跟随重定向,3xx响应会被视为失败。系统只读取响应状态码。
请求体为JSON,包含四个顶层字段:id、type、created_at和data。实际请求体中的键顺序和空白可能与这里的示例不同。
- idstring
- 唯一的事件标识符,用于跳过重复事件。
- typestring
- 上述事件类型之一。
- created_atstring
- 以
Z结尾的ISO 8601格式UTC时间。对于每日收盘事件,该值为已结束日期的23:59:59。 - data.alert_idstring
- 触发的提醒。轮换密钥时不会改变。
- data.kindstring
level_cross、percent_move或daily_close。- data.symbolstring
XAU-USD-SPOT、XAG-USD-SPOT或HG-USD-SPOT。- data.directionstring
- 提醒中设置的
above、below或either。 - data.pricestring
- 触发提醒的价格,以十进制字符串表示。仅用于价位和百分比事件。
- data.observed_atstring
- 观测到价格的时间。仅用于价位和百分比事件。
- data.levelstring
- 你设置的价位。仅用于价位事件。
- data.percent, data.window, data.reference_pricestring
- 你设置的百分比、时间窗口(
1h或24h)以及窗口开始时的价格。仅用于百分比事件。 - data.date, data.closestring
- UTC日期及当天最后一个价格。仅用于每日收盘事件。
请求体示例
price.level_cross
{
"id": "evt_3f0c6a52-6a2e-4b8e-9c1d-2f4b7e9a1c03_level_cross:492841",
"type": "price.level_cross",
"created_at": "2026-10-23T14:02:11.482113Z",
"data": {
"alert_id": "3f0c6a52-6a2e-4b8e-9c1d-2f4b7e9a1c03",
"kind": "level_cross",
"symbol": "XAU-USD-SPOT",
"direction": "above",
"price": "4203.1",
"level": "4200",
"observed_at": "2026-10-23T14:02:11.482113+00:00"
}
}price.percent_move
{
"id": "evt_8a1d52c4-0b7e-4f55-a3d9-61e2c7b8f410_percent_move:492855",
"type": "price.percent_move",
"created_at": "2026-10-23T09:15:40.120934Z",
"data": {
"alert_id": "8a1d52c4-0b7e-4f55-a3d9-61e2c7b8f410",
"kind": "percent_move",
"symbol": "XAU-USD-SPOT",
"direction": "either",
"price": "4168.8",
"percent": "2",
"window": "24h",
"reference_price": "4083.05",
"observed_at": "2026-10-23T09:15:40.120934+00:00"
}
}price.daily_close
{
"id": "evt_c92e0f7b-31a4-4d6a-8e57-a05b3d1c92e8_close:2026-10-22",
"type": "price.daily_close",
"created_at": "2026-10-22T23:59:59Z",
"data": {
"alert_id": "c92e0f7b-31a4-4d6a-8e57-a05b3d1c92e8",
"kind": "daily_close",
"symbol": "XAG-USD-SPOT",
"direction": "either",
"date": "2026-10-22",
"close": "48.62"
}
}webhook.test
{
"id": "evt_test_1792143012345",
"type": "webhook.test",
"created_at": "2026-10-23T09:30:12.345000Z",
"data": {
"alert_id": "3f0c6a52-6a2e-4b8e-9c1d-2f4b7e9a1c03"
}
}验证签名
处理事件前,请检查Webhook-Signature。使用收到的原始请求体字节,不做任何改动。验证前切勿解析JSON后再将其序列化。
- 按逗号拆分
Webhook-Signature,读取t和v1。 - 如果
t比你的时钟快或慢超过300秒,则拒绝请求。 - 将
t、英文句点和原始请求体拼接为消息,即<t>.后接请求体。 - 使用提醒的签名密钥(以
whsec_开头)计算消息的HMAC-SHA256,并将结果编码为十六进制。 - 使用恒定时间比较将结果与
v1比较。如果不一致,则拒绝请求。
Node.js
Python
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const TOLERANCE_SECONDS = 300;
// rawBody: the request body exactly as received (a Buffer), before any JSON parsing.
function verifyWebhook(rawBody, header, secret, now = Math.floor(Date.now() / 1000)) {
const fields = {};
for (const part of String(header ?? "").split(",")) {
const i = part.indexOf("=");
if (i > 0) fields[part.slice(0, i).trim()] = part.slice(i + 1).trim();
}
const t = Number(fields.t);
if (!Number.isInteger(t) || Math.abs(now - t) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(t + ".").update(rawBody).digest();
const given = Buffer.from(fields.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
const seen = new Set(); // use your database in production
createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const rawBody = Buffer.concat(chunks);
const header = req.headers["webhook-signature"];
if (!verifyWebhook(rawBody, header, process.env.WEBHOOK_SECRET)) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody);
if (!seen.has(event.id)) {
seen.add(event.id);
// Hand the event to a queue or background job here, then answer quickly.
console.log(event.type, event.data);
}
res.writeHead(204).end();
});
}).listen(Number(process.env.PORT ?? 3000));import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_webhook(raw_body: bytes, header: str, secret: str, now: int | None = None) -> bool:
"""raw_body is the request body exactly as received, before any JSON parsing."""
fields = dict(p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p)
try:
t = int(fields["t"])
except (KeyError, ValueError):
return False
now = int(time.time()) if now is None else now
if abs(now - t) > TOLERANCE_SECONDS:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(fields.get("v1", "").encode(), expected.encode())
# Flask: verify_webhook(request.get_data(), request.headers.get("Webhook-Signature", ""), secret)
# Django: verify_webhook(request.body, request.headers.get("Webhook-Signature", ""), secret)响应与重试处理
在10秒内返回任意2xx状态码。3xx、4xx、5xx、超时或连接错误均视为一次失败。请保存事件并迅速返回响应,将耗时任务放到后台处理。
- 第一次重试:首次失败后1分钟。
- 第二次重试:上次失败后5分钟。
- 第三次重试:上次失败后30分钟。
- 第四次重试:上次失败后2小时。
- 第五次,也是最后一次重试:上次失败后6小时。
总共在约8.5小时内尝试6次。最后一次尝试失败后,该事件不会再发送,也不会通过电子邮件进行备用通知。每次尝试都会使用新的时间戳重新签名。首次尝试发送时,如果事件已超过一小时,则会被丢弃。发送记录保留30天。
轮换签名密钥
密钥仅在添加提醒时显示一次。如果怀疑密钥泄露或已将其丢失,请在控制台的提醒中选择Rotate secret。新密钥也只显示一次。提醒保留原有标识符,旧密钥立即失效,此后的发送均使用新密钥。为避免发送失败,请在切换期间让服务器同时接受两个密钥,持续几分钟。
常见问题
价格提醒需要哪个套餐?
Plus ($39/月) 和Pro ($89/月) 套餐均包含价格提醒功能,每个套餐各有5个提醒。Free套餐和Basic套餐均不包含价格提醒功能。
单个提醒多久能触发一次?
价位提醒触发后,至少10分钟内不再通知,期间的跨越不会发送。只有该时间已过,且价格已反向跨过价位,回到触发前所在的一侧并距该价位达到0.1%,提醒才会重新就绪。百分比提醒触发一次后,经过1小时且变动消失,才能再次触发。每日收盘按UTC日期每天发送一次。
如果我的服务器宕机了,会怎样?
发送失败后,会在约8.5小时内最多再尝试5次。最后一次尝试失败后,该事件不会再发送,也不会通过电子邮件进行备用通知。
同一个事件可能收到两次吗?
可能。即使你已经处理了事件,也可能重试发送,例如你的响应在10秒后才到达。请保存已处理的事件标识符,并跳过重复事件。
另请参阅
- /docs/stream :通过WebSocket和SSE获取实时价格
- /pricing :套餐与限制