مواصفات الـ 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_*مستقرّة لكل حساب وآمنة للتسجيل.
التحقّق (بالترتيب)
- ارفض إذا كان
|now - X-Payment-Timestamp| > 300ثانية. - احسب
hex(HMAC_SHA256(webhook_secret, "{ts}\n{idempotency}\n{raw_body}"))على البايتات الخام للـ body. لا تُعِد سَلسَلة JSON. - قارِن في زمن ثابت مع
X-Payment-Signature(PHPhash_equals، Nodecrypto.timingSafeEqual، Pythonhmac.compare_digest). - ارفض إذا كنت قد عالجت قيمة
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 من الصفحة الرئيسية.