القائمة

Webhook لتنبيهات الأسعار

استقبل طلب POST موقّعا على عنوان URL الخاص بك عندما يعبر سعر الذهب أو الفضة أو النحاس مستوى محددا، أو يتغير بنسبة معينة، أو يحين إغلاق اليوم. أضف تنبيها في لوحة التحكم، وتحقق من كل طلب قبل التعامل معه.

Plus · $39/moPro · $89/mo

التنبيهات والحدود

يراقب كل تنبيه السعر الفوري لمعدن واحد، الذهب أو الفضة أو النحاس، وفق قاعدة واحدة، ويرسل إلى عنوان 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 فشلا. لا يُقرأ من ردك سوى رمز الحالة.

Content-Type: application/jsonUser-Agent: webhook-delivery/1Webhook-Signature: t=<unix seconds>,v1=<hex>

محتوى الطلب بصيغة 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 ثم تعيد تسلسله قبل التحقق.

  1. قسّم Webhook-Signature عند الفواصل لقراءة t وv1.
  2. ارفض الطلب إذا كان t يسبق ساعتك أو يتأخر عنها بأكثر من 300 ثانية.
  3. كوّن الرسالة من t ثم نقطة ثم محتوى الطلب الخام: <t>. متبوعا بالمحتوى.
  4. احسب HMAC-SHA256 للرسالة باستخدام سر توقيع التنبيه، الذي يبدأ بـ whsec_، ثم حوّل الناتج إلى تمثيل سداسي عشري.
  5. قارن الناتج مع 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 ثوانٍ. خزّن معرّفات الأحداث التي عالجتها وتجاهل المكرر منها.

انظر أيضا