إشعارات Webhook
تلقَّ تحديثات حالة المدفوعات والسحوبات في الوقت الفعلي عبر webhooks موقّعة بـ HMAC.
يرسل نظام 2328.io webhook إلى url_callback الخاص بك كلما تغيرت حالة الدفع. هذه هي الطريقة الموصى بها للحصول على إشعار بالمدفوعات الناجحة.
تنسيق الطلب
- الطريقة:
POST - Content-Type:
application/json - التوقيع: حقل
signفي محتوى الطلب
المحتوى
محتوى webhook مطابق لاستجابة /v1/payment/info، بالإضافة إلى حقل 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",
"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,
"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 | هاش معاملة البلوكتشين، يكون موجودًا فقط بعد تأكيد الدفع |
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",
"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 |
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}) إذا كنت بحاجة إلى يقين.