الانتقال إلى المحتوى
Crypto Merchant

المطورون

Webhooks

تُخطر Webhooks خادمك عند حدوث شيء ما، مثل دفع فاتورة، حتى لا تضطر إلى الاستعلام من API باستمرار.

إضافة نقطة نهاية

في البوابة، افتح المطورون وأضف عنوان HTTPS على خادمك الذي يجب أن يستقبل الأحداث. تحصل كل نقطة نهاية على سر توقيع خاص بها يبدأ بـ whsec_. يمكن لنقطة النهاية استقبال كل الأحداث، بما في ذلك الأحداث التي تُضاف لاحقًا، أو الأحداث التي تختارها فقط. استخدم إرسال حدث تجريبي للتحقق من أن نقطة النهاية تعمل.

الأحداث

invoice.created
تم إنشاء فاتورة.
invoice.confirming
تم رصد دفعة على السلسلة وهي بانتظار التأكيدات.
invoice.paid
الفاتورة مدفوعة ومؤكدة. نفّذ الطلبات عند هذا الحدث.
invoice.underpaid
النقص في الدفعة المؤكدة يتجاوز هامش التسامح الخاص بك.
invoice.expired
انتهى الوقت قبل رصد أي دفعة.
invoice.cancelled
تم إلغاء الفاتورة.
invoice.refunded
أُعيدت العملات إلى العميل: استرداد لفاتورة مدفوعة دفعًا ناقصًا، أو تمت المطالبة برابط استرداد أنشأته. يُرسل مع كل مطالبة، لذا يُرسل الاسترداد الجزئي واحدًا أيضًا.
deposit.pending
تم رصد إيداع على السلسلة في المحفظة الثابتة لأحد عملائك، وهو بانتظار التأكيدات.
deposit.confirmed
تم تأكيد إيداع في محفظة ثابتة وأُضيف إلى رصيدك بعد خصم الرسوم: أضف المبلغ إلى رصيد ذلك العميل الآن.
deposit.dropped
إيداع في محفظة ثابتة تم رصده لكنه لم يُؤكَّد أبدًا (فشل أو أُزيل من السلسلة).
payout.sent
تم إرسال عملية صرف إلى محفظتك، أو أُرسلت حصة شريك إلى محفظة الشريك.
payout.failed
تعذّر إرسال عملية صرف.
swap.completed
اكتملت عملية مبادلة إلى عملة الاحتفاظ الخاصة بك.

الحمولة

يُرسل كل حدث كطلب HTTP POST مع نص JSON:

JSON
{
  "id": "evt_…",
  "type": "invoice.paid",
  "created": "2026-10-28T09:41:07.000Z",
  "data": {
    "id": "inv_…",
    "object": "invoice",
    "status": "paid",
    …
  }
}

id فريد لكل حدث، وtype هو أحد الأحداث المذكورة أعلاه، وcreated هو وقت حدوثه. في أحداث الفواتير، يكون data هو كائن الفاتورة. وفي أحداث الصرف، يكون كائن الصرف. وفي أحداث المبادلة، يصف data عملية المبادلة.

الترويسات

Content-Type
دائمًا application/json.
CM-Signature
الطابع الزمني والتوقيع: t= وقت Unix بالثواني، ثم v1= توقيع HMAC-SHA256 بالنظام الست عشري.
CM-Event-Id
معرّف الحدث، وهو نفسه id في النص.
User-Agent
دائمًا CryptoMerchant-Webhooks/1.
HTTP
POST /webhooks/crypto-merchant HTTP/1.1
Content-Type: application/json
User-Agent: CryptoMerchant-Webhooks/1
CM-Signature: t=1792834867,v1=5f2c…
CM-Event-Id: evt_…

التحقق من التوقيع

تحقق دائمًا من التوقيع قبل الوثوق بأي حدث. يمكن لأي شخص إرسال طلب إلى نقطة النهاية الخاصة بك، لكن Crypto Merchant وحدها تعرف سرّك.

  1. 1

    اقرأ نص الطلب الخام تمامًا كما وصل، قبل تحليل JSON.

  2. 2

    قسّم ترويسة CM-Signature عند الفواصل واقرأ قيمتي t وv1.

  3. 3

    احسب HMAC-SHA256 للطابع الزمني، متبوعًا بنقطة، ثم النص الخام، باستخدام سر نقطة النهاية كمفتاح، بالنظام الست عشري بأحرف صغيرة.

  4. 4

    قارنه بـ v1 باستخدام مقارنة ثابتة الزمن.

  5. 5

    ارفض الأحداث التي يزيد عمر طابعها الزمني على خمس دقائق، حتى لا يمكن إعادة تشغيل الطلبات القديمة.

verify-webhook.js · التحقق من Webhook في Node.js
import crypto from "node:crypto";

/**
 * Checks a Crypto Merchant webhook and returns the event.
 * rawBody: the request body exactly as it arrived (string or Buffer).
 */
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    String(signatureHeader ?? "")
      .split(",")
      .map((part) => part.trim().split("=")),
  );
  const t = parts.t;
  const v1 = parts.v1 ?? "";
  if (!t || !v1) throw new Error("Missing signature");

  const body = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody;
  const expected = crypto
    .createHmac("sha256", secret) // your endpoint secret, whsec_…
    .update(`${t}.${body}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error("Bad signature");
  }
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) {
    throw new Error("Timestamp too old");
  }
  return JSON.parse(body);
}
server.js · Express
import express from "express";
import { verifyWebhook } from "./verify-webhook.js";
import { db } from "./db.js"; // your database, e.g. a pg Pool

// CREATE TABLE webhook_events (
//   id text PRIMARY KEY, type text NOT NULL, payload jsonb NOT NULL,
//   received_at timestamptz NOT NULL DEFAULT now(), processed_at timestamptz
// );

const app = express();

// Keep the raw body: re-serialised JSON would not match the signature.
app.post(
  "/webhooks/crypto-merchant",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;
    try {
      event = verifyWebhook(req.body, req.get("CM-Signature"), process.env.CM_WEBHOOK_SECRET);
    } catch {
      return res.sendStatus(400);
    }

    try {
      // 1. Save the event before replying. A repeat delivery has the same id and is ignored.
      await db.query(
        "INSERT INTO webhook_events (id, type, payload) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
        [event.id, event.type, event],
      );
    } catch {
      return res.sendStatus(500); // not saved: we send it again later
    }

    // 2. It's stored, so confirm it.
    res.sendStatus(200);

    // 3. Do the work from what you saved, here or in a background job.
    //    If your server stops now, the event is still waiting in the table.
    processEvents().catch(console.error);
  },
);

async function processEvents() {
  const { rows } = await db.query(
    "SELECT payload FROM webhook_events WHERE processed_at IS NULL ORDER BY received_at",
  );
  for (const { payload: event } of rows) {
    if (event.type === "invoice.paid") {
      // Mark the order in event.data.order_id as paid. Doing it twice must change nothing.
    }
    await db.query("UPDATE webhook_events SET processed_at = now() WHERE id = $1", [event.id]);
  }
}
verify_webhook.py · Python
import hashlib, hmac, json, time

def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_sec: int = 300) -> dict:
    """Checks a Crypto Merchant webhook and returns the event. raw_body: the request body exactly as it arrived."""
    parts = dict(part.strip().split("=", 1) for part in (signature_header or "").split(",") if "=" in part)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        raise ValueError("Missing signature")
    signed = t.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()  # your endpoint secret, whsec_…
    if not hmac.compare_digest(expected, v1):
        raise ValueError("Bad signature")
    if abs(time.time() - int(t)) > tolerance_sec:
        raise ValueError("Timestamp too old")
    return json.loads(raw_body)
verify_webhook.php · PHP
<?php
// Checks a Crypto Merchant webhook and returns the event.
// Read the raw body with file_get_contents('php://input') and the header with $_SERVER['HTTP_CM_SIGNATURE'].
function verify_webhook(string $rawBody, string $signatureHeader, string $secret, int $toleranceSec = 300): array {
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        $parts[$key] = $value;
    }
    $t = $parts['t'] ?? '';
    $v1 = $parts['v1'] ?? '';
    if ($t === '' || $v1 === '') {
        throw new RuntimeException('Missing signature');
    }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); // your endpoint secret, whsec_…
    if (!hash_equals($expected, $v1)) {
        throw new RuntimeException('Bad signature');
    }
    if (abs(time() - (int) $t) > $toleranceSec) {
        throw new RuntimeException('Timestamp too old');
    }
    return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
}

الاستجابة وإعادة المحاولة

أجب بأي حالة 2xx خلال 10 ثوانٍ لتأكيد استلامك للحدث. أي رد آخر، أو عدم الرد، يُعدّ فشلًا، ونعيد المحاولة بعد:

  1. دقيقة واحدة
  2. 5 دقائق
  3. 15 دقيقة
  4. ساعة واحدة
  5. 3 ساعات
  6. 6 ساعات
  7. 12 ساعة
  8. 24 ساعة

أي ما يصل إلى ثماني محاولات خلال يومين تقريبًا. كل محاولة تُوقّع من جديد بطابع زمني جديد.

ممارسات جيدة

  • احفظ كل حدث قبل أن تجيب، ثم نفّذ العمل انطلاقًا مما حفظته، على سبيل المثال في مهمة تعمل في الخلفية. إذا فشل الحفظ، فأجب بحالة خطأ وسنعيد إرسال الحدث.
  • قد تصل الأحداث أكثر من مرة، وليس دائمًا بالترتيب. استخدم id الخاص بالحدث لتخطي الأحداث التي عالجتها بالفعل.
  • قبل شحن طلب ذي قيمة عالية، يمكنك جلب الفاتورة من API للتحقق مجددًا من حالتها.
  • احتفظ بسر whsec_ على خادمك، مثل مفتاح API.