إشعارات Webhook
تلقَّ تحديثات حالة المدفوعات والسحوبات في الوقت الفعلي عبر webhooks موقّعة بـ HMAC.
يرسل نظام 2328.io webhook إلى url_callback الخاص بك كلما تغيرت حالة الدفع. هذه هي الطريقة الموصى بها للحصول على إشعار بالمدفوعات الناجحة.
تنسيق الطلب
- الطريقة:
POST - Content-Type:
application/json - التوقيع: حقل
signفي محتوى الطلب
المحتوى
يتبع محتوى webhook تنسيق استجابة /v1/payment/info ويضيف tx_explorer_url إلى جانب حقل sign المستخدم للتحقق من التوقيع.
دفعة ناجحة
{
"uuid": "db17d490-15b6-47b9-9015-91d1d8b119f2",
"order_id": "ORDER-12345",
"amount": "180.00000000",
"currency": "RUB",
"url": "https://go.2328.io/db17d490-15b6-47b9-9015-91d1d8b119f2",
"expires_at": "2026-05-09T16:56:58+03:00",
"created_at": "2026-05-09T15:56:58+03:00",
"payer_currency": "TON",
"payer_amount": "0.95256917",
"network": "TON",
"address": "UQA0RevhkCQx-EltyNgPPeG8dqtnCz7ZslOzMdNQlLxVaNBb",
"payment_status": "paid",
"txid": "41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
"tx_explorer_url": "https://tonviewer.com/transaction/41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
"payment_amount": "0.95256917",
"merchant_amount": "0.949711462490000000",
"amount_usd": "2.41324380",
"exchange_rate": "0.01340691",
"sign": "6f8c15b6e53b506d5bfa38ed3fb3b50697af73434262153c02e412541372f04d"
}دفعة ملغاة / فاشلة
عندما لا تكون الدفعة في حالة paid نهائية، تكون قيم txid، payment_amount، وmerchant_amount هي null:
{
"uuid": "48edaf2d-2c49-4638-8f86-88636f661c1f",
"order_id": "ORDER-12345",
"amount": "2800.00000000",
"currency": "RUB",
"url": "https://go.2328.io/48edaf2d-2c49-4638-8f86-88636f661c1f",
"expires_at": "2026-05-09T06:19:04+03:00",
"created_at": "2026-05-09T05:19:04+03:00",
"payer_currency": "ETH",
"payer_amount": "0.01620968",
"network": "ETH-ERC20",
"address": "0x37c20d6d96d130Bc5B33D832e43b8e16aACe0c59",
"payment_status": "cancel",
"txid": null,
"tx_explorer_url": null,
"payment_amount": null,
"merchant_amount": null,
"amount_usd": "37.53934800",
"exchange_rate": "0.01340691",
"sign": "40ce68ad9691ad54e684329d75ab5adaf5b01409a2d18d3e0110b8c1be605342"
}مرجع الحقول
| الحقل | النوع | الوصف |
|---|---|---|
uuid | string | معرّف الدفعة UUID |
order_id | string | معرّف الطلب الخاص بك |
amount | decimal (8 dp) | المبلغ بالعملة الورقية في currency |
currency | string | العملة الورقية التي طلبها التاجر |
url | string | عنوان URL لصفحة الدفع المستضافة |
expires_at | string (ISO 8601) | متى تنتهي صلاحية جلسة الدفع |
created_at | string (ISO 8601) | متى تم إنشاء جلسة الدفع |
payer_currency | string | العملة المشفرة التي يدفع بها الدافع |
payer_amount | decimal (8 dp) | مبلغ العملة المشفرة المتوقع |
network | string | شبكة البلوكتشين |
address | string | عنوان الإيداع |
payment_status | string | إحدى: pending، check، paid، underpaid_check، underpaid، overpaid، cancel، aml_lock (راجع References) |
txid | string | null | هاش معاملة البلوكتشين، يكون موجودًا فقط بعد تأكيد الدفع |
tx_explorer_url | string | null | رابط المعاملة في مستكشف البلوكشين. تكون القيمة null عند عدم وجود txid أو عندما يكون التحويل داخليًا بنظام P2P. |
payment_amount | decimal | null | المبلغ المدفوع فعليًا، يكون موجودًا فقط بعد الدفع |
merchant_amount | decimal (18 dp) | null | المبلغ المُضاف إلى التاجر بعد الرسوم |
amount_usd | decimal (8 dp) | المبلغ بالدولار الأمريكي وقت الإنشاء |
exchange_rate | decimal | سعر صرف العملة المشفرة / الورقية المستخدم |
sign | string (hex) | توقيع HMAC-SHA256 للمحتوى |
التحقق من التوقيع
للتحقق من توقيع webhook:
- استخرج حقل
signمن المحتوى - أزل حقل
signمن الكائن - رمّز الحقول المتبقية كـ JSON
- رمّز JSON بـ Base64
- احسب HMAC-SHA256 من سلسلة Base64 باستخدام API_KEY الخاص بك
- قارن التوقيع المحسوب بقيمة
signباستخدام مقارنة بزمن ثابت
<?php
function verifyWebhookSign(array $data, string $apiKey): bool {
$receivedSign = $data['sign'] ?? '';
unset($data['sign']);
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$base64 = base64_encode($json);
$calculated = hash_hmac('sha256', $base64, $apiKey);
return hash_equals($calculated, $receivedSign);
}
$apiKey = 'YOUR_API_KEY';
$payload = json_decode(file_get_contents('php://input'), true);
if (!verifyWebhookSign($payload, $apiKey)) {
http_response_code(401);
exit;
}
switch ($payload['payment_status']) {
case 'paid':
case 'overpaid':
// Credit the order — check idempotency by order_id first
break;
case 'underpaid_check':
case 'underpaid':
case 'cancel':
break;
}
http_response_code(200);import crypto from "crypto";
import express from "express";
const app = express();
app.use(express.json());
function verifyWebhookSign(payload, apiKey) {
const { sign, ...rest } = payload;
const json = JSON.stringify(rest);
const base64 = Buffer.from(json).toString("base64");
const calculated = crypto
.createHmac("sha256", apiKey)
.update(base64)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(calculated),
Buffer.from(sign || ""),
);
}
app.post("/webhook", (req, res) => {
if (!verifyWebhookSign(req.body, process.env.API_KEY)) {
return res.sendStatus(401);
}
const { order_id, payment_status, txid } = req.body;
if (payment_status === "paid" || payment_status === "overpaid") {
// Credit the order — check idempotency by order_id first
}
res.sendStatus(200);
});import json
import hmac
import hashlib
import base64
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
API_KEY = "YOUR_API_KEY"
def verify_webhook_sign(payload: dict, api_key: str) -> bool:
received = payload.pop("sign", "")
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
b64 = base64.b64encode(body.encode("utf-8")).decode()
calculated = hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(calculated, received)
@app.post("/webhook")
async def webhook(request: Request):
payload = await request.json()
if not verify_webhook_sign(payload, API_KEY):
raise HTTPException(401)
if payload["payment_status"] in ("paid", "overpaid"):
# Credit the order — check idempotency by order_id first
pass
return {"ok": True}package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"net/http"
)
func verifyWebhookSign(body []byte, apiKey string) (map[string]any, bool) {
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
return nil, false
}
received, _ := payload["sign"].(string)
delete(payload, "sign")
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.Encode(payload)
reencoded := bytes.TrimRight(buf.Bytes(), "\n")
b64 := base64.StdEncoding.EncodeToString(reencoded)
h := hmac.New(sha256.New, []byte(apiKey))
h.Write([]byte(b64))
calculated := hex.EncodeToString(h.Sum(nil))
return payload, hmac.Equal([]byte(calculated), []byte(received))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
payload, ok := verifyWebhookSign(body, apiKey)
if !ok {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
status, _ := payload["payment_status"].(string)
if status == "paid" || status == "overpaid" {
// Credit the order — check idempotency first
}
w.WriteHeader(http.StatusOK)
}require "json"
require "openssl"
require "base64"
require "sinatra"
API_KEY = "YOUR_API_KEY"
def verify_webhook_sign(payload, api_key)
received = payload.delete("sign") || ""
body = payload.to_json
b64 = Base64.strict_encode64(body)
calculated = OpenSSL::HMAC.hexdigest("SHA256", api_key, b64)
OpenSSL.fixed_length_secure_compare(calculated, received)
end
post "/webhook" do
payload = JSON.parse(request.body.read)
halt 401 unless verify_webhook_sign(payload, API_KEY)
if %w[paid overpaid].include?(payload["payment_status"])
# Credit the order — check idempotency by order_id first
end
status 200
endتحقق دائمًا من التوقيع قبل إيداع أي أموال للمستخدم. webhook غير موقّع أو موقّع بشكل غير صحيح قد يكون طلبًا مزيفًا.
webhooks السحب
عند تغيّر status لعملية سحب، يرسل النظام webhook بطريقة POST إلى عنوان url_callback الذي تم تمريره عند إنشاء السحب. إذا لم يتم توفير url_callback، فلن يتم إرسال أي webhooks لتلك العملية.
يجب التحقق من webhooks السحب باستخدام Payout API key الخاص بك — وليس API key العادي. خوارزمية التوقيع متطابقة مع webhooks المدفوعات (إزالة sign، ترميز JSON، base64، HMAC-SHA256)، يختلف فقط المفتاح.
المحتوى
{
"uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
"order_id": "4dfdcc84402b1185b71cbe399321533e",
"status": "completed",
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "3.00",
"merchant_amount": "3.00",
"network_amount": "3.00",
"amount_usd": "1.04",
"to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
"memo": null,
"txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
"tx_explorer_url": "https://tronscan.org/#/transaction/9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
"block_number": null,
"error_type": null,
"created_at": "2026-05-07T00:08:38+03:00",
"updated_at": "2026-05-07T00:08:54+03:00",
"from_currency": "USDT",
"debited_amount": "1.050735",
"debited_currency": "USDT",
"sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}مرجع الحقول
| الحقل | النوع | الوصف |
|---|---|---|
uuid | string | معرّف السحب UUID |
order_id | string | معرّف idempotency / المرجع الخاص بك، إذا قمت بتوفيره |
status | string | pending، completed، failed، cancelled (راجع References) |
currency | string | عملة السحب |
network | string | شبكة البلوكتشين |
amount | decimal | مبلغ السحب (بـ currency) |
merchant_amount | decimal | المبلغ المخصوم من رصيد التاجر |
network_amount | decimal | المبلغ المُرسل فعليًا على البلوكتشين |
amount_usd | decimal | القيمة بالدولار الأمريكي وقت السحب |
to_address | string | عنوان البلوكتشين للمستلم |
memo | string | null | memo / تاج الوجهة، إن استُخدم |
txid | string | null | هاش معاملة البلوكتشين، يُحدد عند completed |
tx_explorer_url | string | null | رابط المعاملة في مستكشف البلوكشين. تكون القيمة null عند عدم وجود txid أو عندما يكون التحويل داخليًا بنظام P2P. |
block_number | integer | null | ارتفاع البلوك للمعاملة على البلوكتشين |
error_type | string | null | السبب عند status = failed (مثل aml_risk، راجع References) |
created_at | string (ISO 8601) | متى تم إنشاء عملية السحب |
updated_at | string (ISO 8601) | متى تغيرت الحالة آخر مرة |
from_currency | string | الرصيد المصدري الذي خُصم منه السحب عند استخدام التحويل التلقائي (مثلًا USDT لسحب بـ BTC) |
debited_amount | decimal | المبلغ المخصوم من رصيد from_currency |
debited_currency | string | عملة الخصم |
sign | string (hex) | توقيع HMAC-SHA256 للمحتوى، موقّع باستخدام Payout API key |
أفضل الممارسات
- Idempotency — تحقق دائمًا مما إذا كانت الدفعة قد عولجت بالفعل (بواسطة
order_idأوuuid). قد تصل webhooks عدة مرات. - استجابة سريعة — أعد HTTP 200 بأسرع وقت ممكن. أنقل العمل الثقيل إلى طابور خلفي.
- إعادة المحاولة — إذا لم يتلقَّ النظام HTTP 200، تتم إعادة إرسال webhook بعد دقيقتين. الحد الأقصى 5 محاولات.
- معالجة غير متزامنة — تعامل مع أحداث webhook بشكل غير متزامن لتجنب حجب الاستجابة.
- الأمان — تحقق دائمًا من توقيع
signقبل الوثوق بالمحتوى.
قد تصل webhooks خارج الترتيب. لا تفترض أن أول webhook تستلمه هو الحالة النهائية — أعد الجلب دائمًا عبر /v1/payment/info (أو /v1/payout/status/{uuid}) إذا كنت بحاجة إلى يقين.
عقد التسليم والمعالجة
استخدم الطلب التالي داخل نقطة نهاية الويب هوك الخاصة بك:
- اقرأ محتوى الطلب دون تسجيل الأسرار أو التوقيع الكامل.
- حدد ما إذا كان حدث دفع/محفظة ثابتة أو حدث سحب حتى تختار مفتاح API الصحيح.
- قم بإزالة
sign، وأعد إنتاج بايتات JSON الموثقة، واحسب HMAC-SHA256، وقارن في وقت ثابت. - تحقق من صحة المعرفات المطلوبة، والسلاسل العشرية، وقيم الحالة.
- أدرج سجل صندوق البريد/التكرار بطريقة ذرية. إذا كان موجودًا بالفعل، أعد HTTP 200 دون تكرار التأثيرات الجانبية.
- التزم بتغيير الطلب/دفتر الحسابات وضع البريد الإلكتروني غير الحرج أو التحليلات أو الإشعارات في قائمة الانتظار.
- أرجع HTTP 200 بسرعة.
لا تستدعِ خدمات الطرف الثالث البطيئة أثناء الاحتفاظ بمعاملة التكرار. قد يؤدي انتهاء المهلة بعد الالتزام ولكن قبل الإرجاع إلى محاولة إعادة؛ يجب أن يلاحظ التكرار المفتاح المستلم الملتزم ويصبح بلا عملية.
مفاتيح التكرار الموصى بها
| الحدث | الهوية الأساسية | ملاحظات |
|---|---|---|
| جلسة الدفع | uuid + دليل الحالة/الإصدار | يمكن للفاتورة نفسها إصدار تغييرات حالة مشروعة متعددة. |
| إعادة شحن بدفع جزئي | فاتورة uuid + txid | يمكن أن ينتمي أكثر من تحويل إلى نفس الفاتورة الناقصة الدفع. |
| إيداع المحفظة الثابتة | الشبكة + txid | order_id يُعاد استخدامه بواسطة كل إيداع لتلك المحفظة. |
| الدفع | دفع uuid + الحالة | لا تُنشئ دفعًا ثانيًا من منطق إعادة المحاولة عبر الويب هوك. |
إذا لم يكن لدى مخططك معرف حدث، خزّن تجزئة التحميل المؤكدة كدليل تدقيق إضافي، ولكن لا تستبدل الهويات التجارية أعلاه بالطابع الزمني.
الترتيب والمطابقة
التسليم يتم على الأقل مرة واحدة ورسائل الحالة يمكن أن تأتي بتسلسل غير متوقع. نفذ قواعد أعمال أحادية الاتجاه بدلاً من "يفوز الطلب الأخير":
- لا تحرك الطلب المكتمل أبدًا إلى
checkلأن حدثًا أقدم وصل متأخرًا؛ - السماح لـ
underpaid_checkبتلقي معرفات معاملات إضافية دون تكرار الاعتمادات السابقة؛ - اعتبر
paidوoverpaidحالات تسوية ناجحة، مع الحفاظ على مبالغها المختلفة؛ - احتفظ بـ
underpaidكنتيجة نهائية للمدفوعات الجزئية ما لم يبلغ API الموثوق عنه لاحقًا بحالة أخرى؛ - وجه
aml_lockللمراجعة ولا تدع عامل إعادة المحاولة العامة ينفذه؛ - استعلام معلومات الدفع/السحب كلما تعذر الانتقال، أو كان السياق مفقودًا، أو غير واضح ماليًا.
قم بتشغيل التسوية المجدولة حتى عندما يبدو توصيل الويب هوك صحيًا. قارن بين حالة جهازك المحلي والمبلغ المعتمد مع /v1/payment/info، /v1/static-wallet/transactions، أو /v1/payout/status/{uuid} وابلغ عن الفروقات بدلاً من الكتابة الصامتة على سجل الدفعات.
أمان نقطة نهاية الويب هوك
- تطلب HTTPS والحفاظ على إمكانية الوصول العام للنداء العكسي؛ تُرفض أهداف النداء العكسي الخاصة/المحلية أثناء إنشاء الدفع.
- فرض حد صغير لحجم جسم الطلب ونوع محتوى JSON.
- تحديد معدل قبل العمل المكلف، ولكن اترك مساحة كافية للزيادة المشروعة وإعادة المحاولة.
- لا تفوض أبدًا ويب هوك بواسطة عنوان IP المصدر فقط. قوائم السماح على الشبكة هي دفاع متعدد الطبقات؛ التحقق من HMAC إلزامي.
- قم بحجب
signومفاتيح API والعناوين عند الحاجة طبقًا للسياسة، وبيانات التعريف الشخصية من سجلات التطبيق. - احتفظ بكل من المفاتيح الحالية والمفاتيح البديلة المجدولة صراحة خلال نافذة التدوير المسيطر عليها؛ لا تحزر أبدًا أي مفتاح وقع الحدث.
- أعد جسم خطأ عام عند وجود توقيعات غير صالحة حتى لا تصبح نقطة النهاية مرجعًا للمفاتيح أو الحسابات.