تخطي إلى المحتوى الرئيسي
← العودة إلى الوثائق

مواصفات الـ webhook

صيغة الـ payload، التحقّق من التوقيع، دلالات إعادة المحاولة.

مواصفات الـ webhook

عند قيد طلب ما، يوقّع TriaPay طلب POST إلى webhook URL الذي ضبطته. ملف callback الخاص بإضافة DHRU المدمجة هو الـ receiver القياسي. استخدم هذه الصفحة إن كنت ستكتب الخاص بك.

نقطة النهاية

اضبط الـ URL في صفحة Integration. مع إضافة DHRU المدمجة:

https://yourdhru.com/modules/gateways/callback/triapay.php

أو لتثبيت DHRU تحت مسار فرعي:

https://yourdomain.com/dhru/modules/gateways/callback/triapay.php

يجب أن يكون HTTPS وأن يحلّ إلى IP عام. تُرفض العناوين الخاصة و loopback و link-local.

الـ Headers

Header القيمة
Content-Type application/json
X-Payment-Signature hex(HMAC_SHA256(webhook_secret, signed_input))
X-Payment-Sig-Version 2
X-Payment-Timestamp Unix epoch بالثواني عند التوقيع
X-Payment-Idempotency Idempotency key الخاص بالطلب
X-Payment-Mode live أو sandbox

Signed input

مُدخل HMAC هو:

{timestamp}\n{idempotency}\n{raw_body}

\n هو line feed واحد من ASCII (0x0A). يجب على التكاملات الجديدة تنفيذ v2.

Payload

{
  "event": "payment.credited",
  "invoiceId": 12345,
  "tenantPublicId": "tnt_a3b1c8d4e2f59071",
  "amount": "10.051231",
  "baseAmount": "10.000000",
  "feeDeducted": "0.000000",
  "fees": "0",
  "txHash": "0xabc...",
  "gateway": "triapay",
  "asset": "USDT",
  "chain": "bep20",
  "matchCode": 51231,
  "mode": "live",
  "ts": 1714579200
}
Field ملاحظات
event دائمًا payment.credited.
invoiceId invoice id الخاص بـ DHRU عندك (يُعاد من إنشاء الطلب).
tenantPublicId معرّف مستأجر معتم (tnt_*). مستقرّ لكل حساب.
amount سلسلة عشرية. المبلغ المستلَم بدقّة.
baseAmount سلسلة عشرية. مبلغ الفاتورة الأصلي قبل أي امتصاص للرسوم الزائدة.
feeDeducted سلسلة عشرية. الفرق بين المبلغ المتوقّع والمبلغ المستلَم عندما يدفع العميل فائضًا في رسوم الشبكة. عادةً "0".
fees سلسلة عشرية. حاليًا "0".
txHash مرجع معاملة فريد. tx hash على الـ chain، أو transactionId الخاص بـ Binance Pay، أو deposit id خاص بـ Binance off-chain. استخدمه كمفتاح dedup عند القيد.
gateway دائمًا triapay.
asset USDT أو USDC.
chain trc20، bep20، أو binance_pay. يُظهر BEP20 و TRC20 أيضًا تحويلات Binance الداخلية (off-chain) تحت قيمة الـ chain نفسها.
matchCode كود مطابقة عددي.
mode live أو sandbox. نفس قيمة رأس X-Payment-Mode.
ts نفس قيمة X-Payment-Timestamp.

تغييرات كاسرة

  • (2026-05-10) أصبح payload الـ webhook يستخدم camelCase لجميع الحقول. على الـ receivers المخصّصة التي تقرأ invoice_id / tx_hash / match_code (snake_case) أن تُحدِّث إلى invoiceId / txHash / matchCode. تدعم إضافة DHRU المدمجة (v2.x فأحدث) الصيغة الجديدة جاهزة من الصندوق.
  • أُزيل tenantId (عدد صحيح خام) واستُبدِل بـ tenantPublicId، وهي سلسلة معتمة tnt_* مستقرّة لكل حساب وآمنة للتسجيل.

التحقّق (بالترتيب)

  1. ارفض إذا كان |now - X-Payment-Timestamp| > 300 ثانية.
  2. احسب hex(HMAC_SHA256(webhook_secret, "{ts}\n{idempotency}\n{raw_body}")) على البايتات الخام للـ body. لا تُعِد سَلسَلة JSON.
  3. قارِن في زمن ثابت مع X-Payment-Signature (PHP hash_equals، Node crypto.timingSafeEqual، Python hmac.compare_digest).
  4. ارفض إذا كنت قد عالجت قيمة X-Payment-Idempotency هذه من قبل.

الاستجابة المتوقّعة

Status المعنى
2xx نجاح. تمّ إنهاء الطلب.
4xx / 5xx / network timeout يُعامَل كفشل. يُعاد المحاولة.

ما يهمّ هو status الـ HTTP فقط. محتوى الـ body يُسجَّل لأغراض التشخيص.

سياسة إعادة المحاولة

تُعاد محاولة عمليات التسليم الفاشلة باستخدام backoff. إذا لم تنجح المحاولات، يمكن مطابقة الطلب يدويًا من Admin → Orders → Recredit.

إذا لم يُضبط webhook URL، ينتظر الطلب حتى يُضبط URL صحيح.

مثال PHP

<?php
$secret = 'your_webhook_secret';
$raw    = file_get_contents('php://input');
$sig    = $_SERVER['HTTP_X_PAYMENT_SIGNATURE']  ?? '';
$ts     = $_SERVER['HTTP_X_PAYMENT_TIMESTAMP'] ?? '0';
$idem   = $_SERVER['HTTP_X_PAYMENT_IDEMPOTENCY'] ?? '';

if (abs(time() - intval($ts)) > 300) {
    http_response_code(401);
    exit('stale timestamp');
}

$signed   = $ts . "\n" . $idem . "\n" . $raw;
$expected = hash_hmac('sha256', $signed, $secret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit('bad signature');
}

$payload = json_decode($raw, true);
if (!is_array($payload) || ($payload['event'] ?? '') !== 'payment.credited') {
    http_response_code(400);
    exit('bad event');
}

if (already_credited($payload['txHash'])) {
    echo json_encode(['ok' => true, 'duplicate' => true]);
    exit;
}

credit_invoice(
    $payload['invoiceId'],
    $payload['amount'],
    $payload['txHash'],
    $payload['gateway']
);

echo json_encode(['ok' => true]);

مثال Node.js

import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';

const SECRET = process.env.TRIAPAY_WEBHOOK_SECRET!;

app.post('/webhooks/triapay', express.raw({ type: 'application/json' }), (req, res) => {
  const sig  = req.header('X-Payment-Signature')   ?? '';
  const ts   = parseInt(req.header('X-Payment-Timestamp') ?? '0', 10);
  const idem = req.header('X-Payment-Idempotency') ?? '';

  if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).end('stale');

  const signed   = Buffer.concat([Buffer.from(`${ts}\n${idem}\n`), req.body]);
  const expected = createHmac('sha256', SECRET).update(signed).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(sig,      'hex');
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(401).end('bad signature');
  }

  const payload = JSON.parse(req.body.toString());
  // ... idempotency check + credit ...
  res.json({ ok: true });
});

ملاحظات أمنية

  • عامل webhook secret كأنه كلمة مرور. درّسه فورًا في حال تسرّبه.
  • تحقّق دائمًا من الـ body الخام للطلب، لا من JSON المُحلَّل.
  • استخدم دائمًا مقارنة بزمن ثابت.
  • يجب أن يكون webhook URL عبر HTTPS وأن يحلّ إلى IP عام.

تحتاج المزيد؟

أي شيء يتجاوز هذه الصفحة متاح بموجب اتفاقية شراكة. تواصل معنا عبر WhatsApp من الصفحة الرئيسية.