التنبيهات والحدود
يراقب كل تنبيه السعر الفوري لمعدن واحد، الذهب أو الفضة أو النحاس، وفق قاعدة واحدة، ويرسل إلى عنوان 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 اختبارا في الوقت نفسه.
قراءة الطلب
تستقبل طلب POST على عنوان URL الخاص بك. يجب أن يستخدم العنوان https:// والمنفذ 443 واسم مضيف عاما، وألا يحتوي على اسم مستخدم أو كلمة مرور، وألا يتجاوز 2048 حرفا. يُفحص العنوان قبل كل محاولة تسليم. لا تُتبع عمليات إعادة التوجيه، ويُعد الرد من فئة 3xx فشلا. لا يُقرأ من ردك سوى رمز الحالة.
محتوى الطلب بصيغة JSON، ويضم أربعة حقول في المستوى الأعلى: id وtype وcreated_at وdata. قد يختلف ترتيب المفاتيح والمسافات في المحتوى الفعلي عن الأمثلة هنا.
- idstring
- معرّف فريد للحدث. استخدمه لتجاهل الأحداث المكررة.
- typestring
- أحد أنواع الأحداث المذكورة أعلاه.
- created_atstring
- وقت بتوقيت UTC وفق ISO 8601، ينتهي بـ
Z. للإغلاق اليومي، تكون قيمته 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>.متبوعا بالمحتوى. - احسب HMAC-SHA256 للرسالة باستخدام سر توقيع التنبيه، الذي يبدأ بـ
whsec_، ثم حوّل الناتج إلى تمثيل سداسي عشري. - قارن الناتج مع
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)الرد ومعالجة إعادة المحاولة
أعد أي رمز من فئة 2xx خلال 10 ثوانٍ. يُعد الرد من فئة 3xx أو 4xx أو 5xx، أو انتهاء المهلة، أو خطأ الاتصال، محاولة فاشلة. خزّن الحدث وأعد الرد سريعا، ثم نفّذ العمل البطيء في الخلفية.
- إعادة المحاولة الأولى: بعد دقيقة واحدة (1) من الفشل الأول.
- إعادة المحاولة الثانية: بعد 5 دقائق من الفشل السابق.
- إعادة المحاولة الثالثة: بعد 30 دقيقة من الفشل السابق.
- إعادة المحاولة الرابعة: بعد ساعتين (2) من الفشل السابق.
- إعادة المحاولة الخامسة والأخيرة: بعد 6 ساعات من الفشل السابق.
هذا يعني 6 محاولات خلال نحو 8.5 ساعة. بعد فشل المحاولة الأخيرة، لا يُرسل الحدث مجددا، ولا يوجد بديل عبر البريد الإلكتروني. تُوقّع كل محاولة مجددا بطابع زمني جديد. يُسقط الحدث إذا مضى عليه أكثر من ساعة عند محاولة إرساله لأول مرة. تُحفظ سجلات التسليم لمدة 30 يوما.
تدوير سر التوقيع
يظهر السر مرة واحدة عند إضافة التنبيه. إذا احتملت تسرّبه أو فقدته، اختر Rotate secret في التنبيه من لوحة التحكم. يظهر السر الجديد مرة واحدة. يحتفظ التنبيه بمعرّفه، ويتوقف السر القديم عن العمل فورا، وتستخدم عمليات التسليم اللاحقة السر الجديد. لتجنب فشل التسليم، اجعل خادمك يقبل كلا السرّين لبضع دقائق أثناء التبديل.
أسئلة
ما الخطة المطلوبة لتنبيهات الأسعار؟
تتضمن خطة Plus ($39/شهر) وخطة Pro ($89/شهر) تنبيهات الأسعار، ولكل منهما 5 تنبيهات. أما خطة Free وخطة Basic فلا تتضمن أي منهما تنبيهات للأسعار ضمن الخطة.
كم مرة يمكن أن يُرسل التنبيه الواحد؟
بعد إرساله، يصمت تنبيه المستوى لمدة 10 دقائق على الأقل، ولا تُرسل إشعارات العبور خلالها. لا يصبح جاهزا مجددا إلا بعد انقضاء هذه المدة وعودة السعر إلى الجانب الذي كان عليه قبل العبور، متجاوزا المستوى بنسبة 0.1% في الاتجاه المعاكس. يُرسل تنبيه النسبة مرة، ثم يصبح جاهزا مجددا بعد ساعة واحدة (1) عند زوال التغير. يُرسل الإغلاق اليومي مرة في كل يوم بتوقيت UTC.
ماذا يحدث إذا كان الخادم متوقفا؟
تُعاد محاولة التسليم الفاشلة حتى 5 مرات إضافية خلال نحو 8.5 ساعة. بعد فشل المحاولة الأخيرة، لا يُرسل الحدث مجددا، ولا يوجد بديل عبر البريد الإلكتروني.
هل يمكن أن يصلني الحدث نفسه مرتين؟
نعم. قد تُعاد محاولة التسليم حتى إذا كنت قد عالجت الحدث، مثلا إذا وصل ردك بعد 10 ثوانٍ. خزّن معرّفات الأحداث التي عالجتها وتجاهل المكرر منها.
انظر أيضا
- /docs/stream : أسعار مباشرة عبر WebSocket وSSE
- /pricing : الخطط والحدود