لوحة تحكم الشريك ← Merchant ← «الحصول على مفتاح API». يبدو المفتاح هكذا smk_abcdefghij_plus48chars ويُعرض مرة واحدة فقط. إذا فقدته، فأصدر مفتاحًا جديدًا: يصبح المفتاح السابق غير صالح فورًا.
أضف ترويسة واحدة
Authorization: Bearer your_key.
تحقق من الوصول
اطلب الرصيد. إذا وصلتك استجابة JSON تحتوي على data، فقد تم قبول المفتاح.
{
"data": [
{
"asset_id": "usdt", // دائمًا usdt، لا يوجد رصيد آخر
"available_atomic": "15000000", // 15 USDT بالوحدات الصغرى (6 خانات عشرية)
"available": "15", // القيمة نفسها بالصيغة العشرية
"held_atomic": "0", // محجوز لدفعات صرف غير مكتملة
"held": "0"
}
]
}
إذا فشل الطلب
الاستجابة
السبب
401 unauthorized
المفتاح مفقود أو يحتوي على مسافة أو مقطوع. يجب أن يلي Bearer المعرّف smk_… كاملًا.
401 key_inactive
تم إلغاء المفتاح: صدر مفتاح جديد، لكن الطلب لا يزال يستخدم المفتاح السابق.
خمس قواعد
معظم أخطاء التكامل تنتج عن مخالفة إحدى هذه القواعد.
1. أرسل المبالغ كنصوص (strings)
مرّر إما amount، وهو نص عشري مثل "0.001" أو "15"، أو amount_atomic، وهو عدد صحيح بالوحدات الصغرى: "100000" لـ 0.001 BTC. استخدم حقلًا واحدًا فقط. تتضمن الاستجابات الصيغتين دائمًا: amount بجانب amount_atomic.
مثال
"amount": "0.001" // valid
"amount_atomic": "100000" // also valid
// both at once — 422
معرّف فريد للمحاولة. إذا انقطع الاتصال ولم تعرف النتيجة، فكرر الطلب بالمفتاح نفسه: ستُعاد النتيجة الأصلية ولن تُنشأ عملية ثانية. مطلوب في POST /invoices وPOST /payments وPOST /payouts/preview وPOST /payouts.
مثال
Idempotency-Key: order-42-payment // first attempt → 201, created
Idempotency-Key: order-42-payment // retry → 200, same resource
Idempotency-Key: order-42-payment // different amount → 409 conflict
4. الحقول الفارغة تُحذف
لا تُضمَّن قيم null والنصوص الفارغة. الدفعة غير المدفوعة لا تحتوي على actual_amount أو credited_amount أو fee. تحقق من وجود المفتاح، لا من قيمته.
لا تعني expired أن التحويل لن يصل أبدًا. يستمر رصد العنوان لنحو 6 ساعة إضافية، ويُقيَّد التحويل المتأخر. الحالات النهائية التي يتم فيها القيد: confirmed وconfirmed_underpaid وconfirmed_overpaid.
السيناريوهات
يغطي كل سيناريو الدورة كاملة: ترتيب الطلبات، وشكل الاستجابة، ومعنى الحقول. التعليقات في أمثلة JSON للتوضيح فقط ولا تظهر في الاستجابات الحقيقية.
استقبال المدفوعات
العميل يدفع بـ BTC مقابل طلب بقيمة 60 دولارًا
الطلب order-42. تحتاج إلى نحو 60 USDT؛ وبسعر BTC يقارب 60,000 يكون المبلغ المتوقع من العميل 0.001 BTC.
1
احصل على قائمة الأصول المتاحة
تُرجع الاستجابة رمز الأصل وdecimals، أي عدد الخانات بعد الفاصلة العشرية.
{
"data": [
{
"asset_id": "btc", // ← استخدمه في الطلبات اللاحقة
"symbol": "BTC", // للعرض على المستخدم
"network": "bitcoin", // يُستخدم في دفعات الصرف
"decimals": 8, // 1 BTC = 100000000 وحدة صغرى
"price_usd": "60000" // استرشادي، ليس لحساب عروض الأسعار
},
{
"asset_id": "usdt",
"symbol": "USDT",
"network": "TRON",
"decimals": 6,
"price_usd": "1.00"
}
]
}
2
ثبّت سعر الصرف لمدة 10 دقائق
يثبّت سعر الصرف بينما يكون العميل في صفحة الدفع. لا تُنشأ دفعة أو عنوان في هذه الخطوة، بل يُحسب عرض السعر فقط. الاستجابة هي مبلغ USDT الذي سيُقيَّد مقابل 0.001 BTC.
اختياري: لا تُنشأ أي عملية. يمكنك إرساله لإعادة استخدام عرض السعر نفسه.
الاستجابة
{
"data": {
"credit_lock_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
// ← احفظه للخطوة التالية
"asset_id": "btc",
"expected_amount": "0.001", // المبلغ المتوقع من العميل
"expected_amount_atomic": "100000",
"credit_usdt": "59.4", // ← المبلغ الذي سيُقيَّد
"credit_usdt_atomic": "59400000",
"expires_at": "2026-08-11T17:20:00+00:00"
// ← بعد هذا الوقت يصبح credit-preview غير صالح
}
}
اعرض للعميل expected_amount، أي كمية BTC التي يجب إرسالها.
3
أنشئ الدفعة واحصل على العنوان
تنشئ هذه الخطوة الدفعة وتُرجع عنوان الإيداع. لا ترسل المبلغ في هذا الطلب، فهو محدد مسبقًا في credit-preview في الخطوة 2.
أخبر العميل أن الشبكة هي TRC20. لن تُقيَّد USDT المرسلة على شبكة أخرى إلى هذا العنوان.
أرسل العميل مبلغًا أقل من المطلوب
كان المتوقع 0.001 BTC، ووصل 0.0009، مثلًا لأن محفظة العميل خصمت عمولة الشبكة من المبلغ.
1
تحقق من الدفعة
إذا وصل أقل من expected_amount لكن المبلغ يظل كافيًا للقيد، تكون الحالة confirmed_underpaid ويكون credited_amount أقل من عرض السعر. إذا كان المبلغ صغيرًا جدًا، فلا يُقيَّد شيء.
{
"data": {
"payment_id": "pay_01HZX...",
"status": "expired", // انتهت النافذة
"asset_id": "btc",
"expected_amount": "0.001",
"deposit_address": "bc1qexampledepositaddress000000000",
"expires_at": "2026-08-11T18:00:00+00:00"
// لا يوجد actual_amount ولا credited_amount، فلم يصل أي تحويل
}
}
2
دفع العميل متأخرًا
إذا وصل التحويل خلال نافذة الرصد الإضافية، تنتقل الدفعة إلى confirmed رغم ذلك. لا تحذف الطلب عند expired: علّمه كمتأخر واستمر في الاستعلام.
الدفع الناقص لا يغلق الفاتورة: تكون الحالة underpaid، وينتظر العنوان نفسه باقي المبلغ (USDT: 24 ساعة). إذا لم يصل الباقي في الوقت المحدد، تنتهي الفاتورة بالحالة partially_paid.
ملاحظة
لا تذكر المسارات الداخلية في واجهة العميل. تعرض صفحة الدفع العنوان والمبلغ والشبكة فقط.
لا يوجد API للاسترداد. التحويل المكتمل الثاني قيد منفصل، وليس عكسًا للأول.
دفعات الصرف
تحويلات صادرة من رصيدك بـ USDT.
أرسل للعميل 15 USDT على شكل BTC
amount هو ما يُخصم من رصيدك، وليس ما يحصل عليه المستلم. يُرجع preview مبلغ المستلم.
1
تحقق من الرصيد المتاح
استخدم available، وليس available مضافًا إليه held. الرصيد held محجوز لدفعات صرف غير مكتملة ولا يمكن إنفاقه.
{
"data": {
"payout_id": "payout_01HZX...", // ← المعرّف نفسه كما في الاستجابة الأولى
"external_id": "wd-77",
"debit_amount": "15",
"status": "processing", // ربما تغيّرت الحالة أثناء الانتظار
"created_at": "2026-08-11T17:10:00+00:00"
}
}
HTTP 201: أُنشئت العملية في هذا الطلب. HTTP 200: كانت موجودة مسبقًا. كلا الرمزين يعني النجاح.
2
إذا تغيّرت المعاملات
يُرفض تكرار المفتاح نفسه بمبلغ أو عنوان مختلف. هذا يمنع عدم تطابق المعاملات.
إذا كانت هذه دفعة صرف جديدة، فاستخدم Idempotency-Key جديدًا.
الاستجابة
{
"error": {
"code": "conflict",
"message": "Idempotency-Key was used with different parameters."
}
}
ملاحظة
أنشئ المفتاح مرة واحدة واحفظه مع الطلب. إنشاء مفتاح جديد قبل كل إرسال يُعطّل الحماية من التكرار.
عند HTTP 429، أعد المحاولة بالمفتاح نفسه بعد بضع ثوانٍ.
Webhooks
عندما تتغير فاتورة أو إيداع أو دفعة صرف أو تحويل أو الرصيد، ترسل SnapEX طلب POST إلى عنوان URL الخاص بك. الـ Webhook إشارة وليس مصدر الحقيقة: تحقق من التوقيع، وللقرارات المهمة أعد قراءة الكائن عبر الـ API.
الإعداد
حدّد عنوان URL في لوحة التحكم
لوحة تحكم الشريك ← الفواتير ← بطاقة «Webhook»: أدخل عنوان URL واضغط «حفظ». عنوان URL واحد لكل حساب، ويستقبل جميع أنواع الأحداث الواردة في الجدول أدناه.
احفظ مفتاح التوقيع السري
عند الحفظ الأول تعرض لوحة التحكم المفتاح السري whsec_… مرة واحدة فقط. احتفظ به على خادمك فقط. تغيير عنوان URL لاحقًا يُبقي على المفتاح السري نفسه.
متطلبات عنوان URL
عنوان URL مطلق يبدأ بـ https:// مع مضيف عام: بدون login:password في العنوان، وبدون localhost، ويجب ألا يُحلّ المضيف إلى عنوان IP خاص أو محجوز. يُفحص عنوان URL عند الحفظ ومرة أخرى قبل كل محاولة تسليم.
ما الذي يصل
الترويسة
المعنى
Webhook-Id
معرّف فريد للحدث (UUID)، يساوي id في الـ body. لا يتغير عند إعادة المحاولة أو الإرسال اليدوي، فاستخدمه لإزالة التكرار.
Webhook-Timestamp
وقت Unix بالثواني لحظة إرسال هذه المحاولة. كل إعادة محاولة تحمل قيمة جديدة وتوقيعًا جديدًا.
Webhook-Signature
v1= متبوعًا بقيمة HMAC-SHA256 بصيغة hex وأحرف صغيرة للنص «{Webhook-Timestamp}.{raw body}». المفتاح هو المفتاح السري كاملًا، بما في ذلك البادئة whsec_.
Content-Type
دائمًا application/json. الـ body بصيغة UTF-8 JSON.
يعتمد data على النوع: invoice.* هي الفاتورة بالشكل نفسه كما في GET /invoices/{id} (يكون attempts فارغًا، والمحاولة الحالية في payment)؛ payment.* هو الإيداع (payment_id، external_id، status، المبالغ)؛ payout.* هي دفعة الصرف (payout_id، external_id، status، المبالغ)؛ conversion.* هو التحويل (conversion_id، status، المبالغ)؛ balance.changed يتضمن asset_id وentry_type وavailable_atomic وheld_atomic بعد التغيير.
أنواع الأحداث
الحدث
المعنى
invoice.created
أُنشئت الفاتورة، ولم تُختر طريقة دفع بعد (open).
invoice.awaiting
اختيرت طريقة الدفع وصدر العنوان (awaiting).
invoice.underpaid
وصل جزء من المبلغ، وينتظر العنوان نفسه الباقي (underpaid).
invoice.paid
دُفع المبلغ كاملًا وقُيّد (paid، نهائية). الإشارة الوحيدة لشحن الطلب.
invoice.partially_paid
دفع العميل جزءًا فقط، وانتهت نافذة الاستكمال، وقُيّد الجزء المستلم لك (partially_paid، نهائية).
invoice.expired
انتهت نافذة الفاتورة دون دفع (expired، نهائية).
invoice.extra_payment
وصل تحويل آخر بعد دفع الفاتورة، ويُقيَّد بشكل منفصل.
payment.created
أُنشئ إيداع.
payment.detected
ظهر التحويل على الشبكة لكنه لم يُقيَّد بعد.
payment.credited
قُيّد الإيداع (إحدى حالات confirmed*).
payment.expired
انتهت نافذة الإيداع.
payout.created
أُنشئت دفعة صرف وحُجزت الأموال.
payout.processing
دفعة الصرف قيد الإرسال.
payout.completed
وصلت الأموال إلى المستلم.
payout.failed
فشلت دفعة الصرف.
conversion.completed
اكتمل التحويل.
conversion.failed
فشل التحويل كليًا أو جزئيًا.
balance.changed
سُجّل قيد في الرصيد: قيد دائن أو حجز أو خصم.
اشحن الطلب فقط عند invoice.paid، فهو يُرسل فقط عند دفع المبلغ كاملًا. أما invoice.underpaid وinvoice.partially_paid فيعنيان أن المبلغ غير مكتمل.
الفواتير المنشأة مع environment=test محاكاة: تمر بالحالات نفسها وترسل الـ Webhooks نفسها (data.environment = "test")، لكنها لا تغيّر الرصيد الحقيقي أبدًا.
التسليم وإعادة المحاولة
النجاح هو أي استجابة 2xx. أي رمز آخر أو انتهاء مهلة أو خطأ في الشبكة يُعد فشلًا. لا يُفسَّر body الاستجابة.
المهل: 5 ثوانٍ للاتصال، و15 ثانية للطلب بأكمله.
لا تُتبع عمليات إعادة التوجيه: 301/302 يُعد فشلًا. حدّد عنوان URL النهائي.
حتى 5 محاولات لكل حدث: المحاولة الأولى، ثم إعادة المحاولة بعد نحو 10 ثوانٍ، ودقيقة، و5 دقائق، و15 دقيقة من كل فشل. بعد الفشل الخامس يتوقف التسليم التلقائي.
تعرض البطاقة نفسها «آخر عمليات التسليم»: الحدث، ورمز HTTP أو الخطأ، والوقت، ورقم المحاولة. زر «إعادة المحاولة» يرسل الحدث مجددًا بـ Webhook-Id نفسه.
التسليم من نوع at-least-once: قد يصل الحدث الواحد عدة مرات، وقد تصل الأحداث بغير ترتيبها (قد يصل حدث أُعيدت محاولته بعد حدث أحدث).
التحقق من التوقيع
اقرأ body الطلب الخام كبايتات، قبل تحليل JSON. لن يطابق JSON المُعاد تسلسله التوقيع.
احسب HMAC-SHA256 على «{Webhook-Timestamp}.{raw body}» باستخدام مفتاحك السري whsec_… كمفتاح، ثم رمّزه بصيغة hex.
قارن «v1=» + hex مع ترويسة Webhook-Signature باستخدام دالة ثابتة الزمن: hash_equals أو crypto.timingSafeEqual أو hmac.compare_digest.
ارفض أي Webhook-Timestamp يختلف عن ساعتك بأكثر من 5 دقائق (موصى به). تحمل إعادات المحاولة طابعًا زمنيًا جديدًا، لذا لن يؤثر ذلك عليها.
PHP
<?php
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // whsec_… من لوحة التحكم
$body = file_get_contents('php://input'); // الـ body الخام، قبل تحليل JSON
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (! ctype_digit($timestamp)
|| abs(time() - (int) $timestamp) > 300
|| ! hash_equals($expected, $signature)) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// تخطَّ إذا كان Webhook-Id هذا قد عولج سابقًا؛ ونفّذ العمل الثقيل في طابور
http_response_code(200);
Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
const secret = process.env.SNAPEX_WEBHOOK_SECRET; // whsec_… من لوحة التحكم
// الـ body الخام، قبل تحليل JSON
app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('Webhook-Timestamp') || '';
const signature = Buffer.from(req.get('Webhook-Signature') || '');
const expected = Buffer.from('v1=' + crypto.createHmac('sha256', secret)
.update(timestamp + '.').update(req.body).digest('hex'));
const ok = /^\d+$/.test(timestamp)
&& Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300
&& signature.length === expected.length
&& crypto.timingSafeEqual(signature, expected);
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// تخطَّ إذا كان Webhook-Id هذا قد عولج سابقًا؛ ونفّذ العمل الثقيل في طابور
res.sendStatus(200);
});
Python
import hashlib, hmac, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["SNAPEX_WEBHOOK_SECRET"].encode() # whsec_… من لوحة التحكم
@app.post("/hooks/snapex")
def snapex_webhook():
body = request.get_data() # الـ body الخام، قبل تحليل JSON
timestamp = request.headers.get("Webhook-Timestamp", "")
signature = request.headers.get("Webhook-Signature", "")
expected = "v1=" + hmac.new(SECRET, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
if not (timestamp.isascii() and timestamp.isdigit()) \
or abs(time.time() - int(timestamp)) > 300 \
or not hmac.compare_digest(expected.encode(), signature.encode()):
abort(400)
event = request.get_json()
# تخطَّ إذا كان Webhook-Id هذا قد عولج سابقًا؛ ونفّذ العمل الثقيل في طابور
return "", 200
قائمة التحقق للمعالج
تحقق من التوقيع على الـ body الخام بمقارنة ثابتة الزمن، وأجب بـ 400 إذا لم يتطابق.
ارفض الطلبات التي يكون Webhook-Timestamp فيها أقدم من 5 دقائق.
احفظ قيم Webhook-Id التي عولجت وتجاهل التكرارات. لا تعتمد على الترتيب: قارن الحالة في الحدث بما لديك بالفعل.
أجب بـ 2xx بسرعة، وقبل 15 ثانية بوقت كافٍ، ونفّذ العمل الثقيل بشكل غير متزامن.
بعد وصول Webhook يمكنك إعادة قراءة الكائن عبر الـ API للتأكد: GET /invoices/{id} أو GET /payments/{id} أو GET /payouts/{id}.
الحالات
شارة «نهائية» تعني أن الحالة لن تتغير. الاستثناء الوحيد هو الإيداعات منتهية الصلاحية (expired): فقد يصل تحويل متأخر.
الإيداعات
الحالة
المعنى
ما الذي تفعله
awaiting_deposit
صدر العنوان، بانتظار التحويل
اعرض للعميل العنوان والمبلغ
detected
التحويل ظاهر على الشبكة، ولم يُقيَّد بعد
انتظر. لا تغلق الطلب
processing
جارٍ التحويل إلى USDT
انتظر
confirmed
وصل المبلغ المتوقع وقُيّدت الأموال
أغلق الطلب
نهائية
confirmed_underpaid
وصل أقل من المتوقع، ضمن النطاق المسموح
تحقق من credited_amount وقرّر إن كان كافيًا
نهائية
confirmed_overpaid
وصل أكثر من المتوقع، وقُيّد وفق credit-preview
أغلق الطلب. تواصل مع الدعم بشأن المبلغ الزائد
نهائية
confirmed_mismatch
محفوظة للتوافق
لا تُستخدم في الدفعات الجديدة
نهائية
manual_review
مطلوب مراجعة يدوية
انتظر المراجعة؛ لا تنشئ دفعة ثانية
expired
انتهت نافذة الدفع، ويُرصد العنوان لنحو 6 ساعة إضافية
لا تحذف الطلب؛ استمر في الاستعلام
failed
فشل من جهة المزوّد
أنشئ دفعة جديدة
نهائية
cancelled
أُلغيت الدفعة
أنشئ دفعة جديدة
نهائية
refunded
استردها المزوّد
طابِق الرصيد؛ وتواصل مع الدعم إذا لم يتطابق
نهائية
الفواتير
الحالة
المعنى
ما الذي تفعله
open
أُنشئت الفاتورة، ولم تُختر طريقة الدفع
أعطِ العميل pay_url أو اختر asset_id بنفسك
awaiting
صدر العنوان، بانتظار التحويل
اعرض العنوان والمبلغ؛ واستعلم عن الحالة
underpaid
وصل جزء من المبلغ؛ العنوان نفسه
اطلب من العميل إرسال الباقي إلى هذا العنوان
paid
قُيّد أول تحويل كامل
أغلق الطلب
نهائية
partially_paid
دفع العميل جزءًا فقط، وانتهت نافذة الاستكمال، وقُيّد الجزء المستلم لك
لا تشحنه كطلب مدفوع؛ سوِّ الباقي مع العميل
نهائية
expired
انتهت نافذة الفاتورة
أنشئ فاتورة جديدة
نهائية
دفعات الصرف
الحالة
المعنى
ما الذي تفعله
reserved
حُجزت الأموال لدفعة الصرف
انتظر
submitting
أُرسلت إلى المزوّد
انتظر
processing
المزوّد يرسل على البلوكتشين
انتظر
completed
وصلت الأموال إلى المستلم
تم
نهائية
manual_review
النتيجة غير واضحة
لا تنشئ دفعة صرف أخرى؛ انتظر المراجعة
failed
فشل التحويل، وعادةً ما يُحرَّر المبلغ المحجوز
تحقق من الرصيد؛ وأعد المحاولة بـ Idempotency-Key جديد عند الحاجة
نهائية
الأخطاء
الاستجابة الناجحة تكون دائمًا في data، والخطأ يكون دائمًا في error. لا توجد أغلفة أخرى.
شكل الخطأ
{
"error": {
"code": "validation_error", // رمز قابل للقراءة آليًا، انظر الجدول أدناه
"message": "The request is invalid.",
"details": { // غير موجود دائمًا
"destination_address": ["The destination address field is required."]
// المفتاح هو الحقل الذي فشل في التحقق
}
}
}
HTTP
code
متى
ما الذي تفعله
401
unauthorized
المفتاح مفقود أو غير صالح
تحقق من ترويسة Authorization: Bearer smk_… كاملة، دون مسافات أو فواصل أسطر
401
key_inactive
المفتاح ملغى
أُلغي المفتاح السابق. استخدم المفتاح الحالي من لوحة التحكم
401
key_expired
انتهت صلاحية المفتاح
أصدر مفتاحًا جديدًا
404
not_found
المعرّف غير موجود أو يخص تاجرًا آخر
استخدم payment_id أو payout_id من استجابتك أنت
409
conflict
Idempotency-Key مرتبط بعملية أخرى، أو external_id مستخدم بالفعل
للعملية الجديدة استخدم مفتاحًا جديدًا. ولإعادة المحاولة استخدم المفتاح نفسه والمعاملات نفسها
422
validation_error
حقول غير صالحة، أو credit-preview منتهي الصلاحية، أو رصيد غير كافٍ، أو Idempotency-Key مفقود
راجع details لمعرفة الحقل والسبب
429
rate_limited
تم تجاوز حد الطلبات
أعد المحاولة لاحقًا بـ Idempotency-Key نفسه
500
internal_error
خطأ داخلي في الخادم
أعد المحاولة لاحقًا بـ Idempotency-Key نفسه. إذا استمر الخطأ، فتواصل مع الدعم وأرفق X-Request-Id