اقبل المدفوعات بالعملات الرقمية
تُسوّي Paysell مدفوعات TON وUSDT على شبكة TON. تُنشئ فاتورة، نمنحك رابطًا، وتحصل على استدعاء رجوع (callback) موقّع بمجرد تأكيد الأموال على السلسلة وإضافتها إلى رصيدك.
نظرة عامة#
ما الذي تفعله Paysell، وما لا تفعله.
Paysell معالج مدفوعات، وليست محفظة. لن تتعامل أبدًا مع مفاتيح خاصة، أو تراقب البلوك تشين، أو تقرر متى تصبح المعاملة نهائية — هذا الجزء نتولاه نحن.
تحصل كل فاتورة على عنوان استلام خاص بها. عندما يدفعها المشتري، ننتظر تأكيد الشبكة للتحويل، نخصم عمولتنا، ونضيف الباقي إلى رصيدك. يمكنك السحب إلى أي عنوان تريده.
كيف تتم عملية الدفع#
ست خطوات، معظمها من جانبنا.
ست خطوات، معظمها من جانبنا:
- 1
يضغط عميلك على الدفع
يستدعي خادمك واجهتنا البرمجية بالمبلغ ومرجع طلبك الخاص.
- 2
نمنح عنوانًا
يُؤخذ عنوان استلام جديد من مجموعة مُعدة مسبقًا ويُربط بهذه الفاتورة. يخص العنوان فاتورة واحدة مفتوحة فقط، وبهذا يُطابَق الدفع معها.
- 3
يرسل العميل العملات
يمسح رمز QR أو ينسخ العنوان. وجّهه إلى
payment_urlالتي نُعيدها، وستتولى الصفحة كل شيء عنك — المبلغ والعنوان ورمز QR والعد التنازلي والحالة المباشرة. - 4
نكتشف التحويل
يُستعلَم عن مصدرين مستقلين لبيانات البلوك تشين، وتُقارَن إجاباتهما. إذا اختلفا، نتوقف بدلاً من اختيار الإجابة الأنسب.
- 5
ننتظر النهائية (finality)
الإدراج في السلسلة الرئيسية بالإضافة إلى ثلاث كتل فوقها. نحو خمس عشرة ثانية — فالدفعة التي تبدو مكتملة ثم تختفي لاحقًا ستكون خسارتك، لذا لا نخاطر بذلك.
- 6
تُضاف إلى الرصيد، ويصلك إشعار
تُخصم العمولة، ويصل الباقي إلى رصيدك، ويُرسَل ويب هوك موقّع إلى خادمك يحمل
order_idالخاص بك.
من الدفع إلى الاستدعاء الرجعي: نحو دقيقة واحدة — نحو خمس عشرة ثانية لتأكيدات الشبكة، والباقي هو مسحنا للعناوين المراقَبة.
إلى أين يذهب المال#
العمولة، وعلى أي أساس تُحسب.
العمولة 0.2%، وتُثبَّت لمتجرك عند لحظة تسجيله. إذا تغيّر السعر القياسي لاحقًا، لن يتغيّر سعرك — فهو مكتوب في كل فاتورة كرقم، لا كإشارة إلى إعداد.
تُحسَب العمولة على ما وصل فعليًا، لا على ما طلبته الفاتورة. أصدرت فاتورة بـ5 USDT ووصلك 20، تُحسَب العمولة على 20. إذا دُفع أقل، تُحسَب على ما وصل.
Invoice: 5.000000 USDT
Received: 20.000000 USDT (the buyer sent more)
Fee 0.2%: 0.040000 USDT (on 20, not on 5)
Credited: 19.960000 USDTيُضاف المبلغ الزائد بالكامل إلى الرصيد — نحن لا نحتفظ بالفرق. الدفع الناقص يُبقي الفاتورة مفتوحة ليتمكن المشتري من إتمام المبلغ إلى نفس العنوان.
البدء السريع#
خمس دقائق حتى أول فاتورة لك.
خمس خطوات. اثنتان منها نقرات في لوحة حسابك، وواحدة طلب واحد من خادمك، والأخيرتان تحدثان من تلقاء نفسيهما.
- 1
أنشئ متجرًا
من لوحة حسابك. يبدأ فورًا في قبول المدفوعات — دون انتظار المراجعة. يجري التحقق بهدوء في الخلفية ولا يُقيّد سوى عمليات السحب، لا المدفوعات الواردة.
- 2
أصدر مفتاح API
متجرك ← مفاتيح API ← مفتاح جديد. يُعرض المفتاح وسرّ الويب هوك مرة واحدة فقط، ولا يُعرضان بعدها أبدًا. احفظهما كما تحفظ كلمة مرور قاعدة بيانات، ولا ترسلهما أبدًا إلى المتصفح.
- 3
أنشئ فاتورة
طلب واحد من خادمك، ورابط واحد في المقابل. الأمثلة الأربعة أدناه ترسل الشيء نفسه تمامًا.
- 4
أرسل المشتري إلى
payment_urlهذه هي صفحة الدفع بكاملها — المبلغ والعنوان ورمز QR والعد التنازلي والحالة الحيّة — ولا شيء عليك بناؤه. لمعرفة ما يراه المشتري فعليًا انظر صفحة الدفع.
- 5
انتظر الويب هوك
بمجرد تأكيد المال على السلسلة وقيده في رصيدك، نرسل إلى خادمك طلب POST يحمل حدث
payment.creditedموقَّعًا. تحقق من التوقيع، ثم علّم الطلب كمدفوع — لكن فقط عندما تكونdata.statusهيpaidأوoverpaid. انظر الويب هوك.
The same request, four ways
curl -X POST https://paysell.me/api/merchant/v1/invoices \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"asset": "USDT_TON",
"amount": "5",
"order_id": "order-1042",
"idempotency_key": "order-1042"
}'أعِد توجيه المشتري إلى payment_url في الاستجابة. انتهى الأمر — والباقي سيصل عبر ويب هوك.
What to do next
- Write the webhook receiver — Webhooks and A complete receiver. Nothing else on this page matters as much: it is what turns a payment into a paid order.
- Handle
underpaidandoverpaid, not justpaid— see Status reference. - Read Typical mistakes, then walk the go-live checklist before you point real customers at it.
المصادقة#
مفتاح API الخاص بك، وكيفية استخدامه.
يحمل كل طلب مفتاحك في ترويسة Authorization:
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxAكل مفتاح يُصدَر هنا يبدأ بـsk_live_. أما بادئة sk_test_ فلا توجد إلا في نسخة منشورة موجَّهة إلى شبكة الاختبار، ونحن لا نقدّم مثل هذه النسخة — انظر الاختبار. نحفظ تجزئة (hash) أحادية الاتجاه، لا المفتاح نفسه، فلا أحد، ولا حتى نحن، يمكنه عرضه لك مجددًا. فقدته؟ أصدر واحدًا جديدًا وأبطل القديم.
يُشتَق المتجر من المفتاح، ولهذا لا يحمل أي طلب معرّف متجر. لا يمكن لمفتاح أن يعمل إلا على متجره الخاص.
يحمل المسار رقم إصدار: /api/merchant/v1/…. داخل الإصدار الواحد نضيف حقولًا فقط — لا يُعاد تسمية شيء ولا يتغيّر معنى شيء في صمت. أي تغيير قد يكسر شيفرتكم يحصل على بادئة جديدة، /v2، ويظل /v1 يعمل مدةً معلنة.
Endpoints at a glance#
Four calls, three of them authenticated.
This is the whole merchant API. Balances, payouts and history are not in it — they live in your account area, where a person is looking at them.
| Endpoint | Method | Auth | What it does |
|---|---|---|---|
| /invoices | POST | API key | Open an invoice and get a payment link. Details. |
| /invoices/{invoice_id} | GET | API key | Read one invoice's current state. Details. |
| /invoices/{invoice_id}/cancel | POST | API key | Close an invoice that is still open and free its address. Details. |
| /public/invoices/{invoice_id} | GET | none | What the hosted checkout page reads. Only needed if you build your own. Details. |
Every path is relative to https://paysell.me/api/merchant/v1. There is no list endpoint and no refund endpoint — see Refunds.
إنشاء فاتورة#
POST /api/merchant/v1/invoices
/api/merchant/v1/invoicesمحتوى الطلب
| الحقل | النوع | إلزامي | الوصف |
|---|---|---|---|
| asset | string | نعم | إما TON أو USDT_TON. |
| amount | string | نعم | الوحدات العادية للعملة، كسلسلة نصية: "5" تعني 5 USDT. بعدد خانات عشرية لا يتجاوز ما تملكه العملة. انظر المبالغ. |
| order_id | string | لا | مرجعك الخاص، حتى 200 حرف. يعود في كل ويب هوك — وهكذا تُطابق الدفعة بالطلب. |
| description | string | لا | حتى 1000 حرف. يُعرض للمشتري في صفحة الدفع. |
| ttl_minutes | number | لا | مدة بقاء الفاتورة قابلة للدفع، بالدقائق. 1–1440؛ وإن أغفلته طُبِّق الافتراضي — وهو اليوم ساعتان. |
| idempotency_key | string | لا | حتى 200 حرف. أرسل القيمة نفسها عند إعادة المحاولة وستحصل على الفاتورة نفسها بدلاً من فاتورة ثانية. وهو حقل في جسم الطلب، لا ترويسة Idempotency-Key — فهذه الترويسة لا تُقرأ هنا. |
الاستجابة · 201
{
"invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
"payment_url": "https://paysell.me/pay/12c22c1a-a496-4c1e-abe3-72661ef8706e",
"address": "UQAvDJp7QDwqRcuNQBiK2GhBt71Xh1_UMYPCzMkQAoBPmZKl",
"asset": "USDT_TON",
"amount": "5",
"amount_minor": "5000000",
"status": "pending",
"paid": "0",
"paid_minor": "0",
"order_id": "order-1042",
"description": "Pro subscription",
"expires_at": "2026-09-06T17:20:55Z",
"created_at": "2026-09-06T15:20:55Z"
}كيفية استخدامها في طلبك
| الحقل | ما ينبغي فعله به |
|---|---|
| invoice_id | احفظه مع طلبك. هو ما يُعرّف الدفعة في كل مكان آخر. |
| payment_url | وجّه المشتري إلى هنا. لا حاجة لبناء أي شيء آخر. |
| address | فقط إذا كنت تبني صفحة الدفع الخاصة بك. اعرضه تمامًا كما وصل — انظر التحذير أدناه. |
| amount | المبلغ بالوحدات العادية، تمامًا كما أرسلته. اعرض هذا. |
| amount_minor | المبلغ نفسه عددًا صحيحًا بالوحدة الصغرى. احسب بهذا. |
| expires_at | اعرض عدًا تنازليًا. بعد انقضائه تتوقف مراقبة العنوان لهذه الفاتورة. |
| status | هنا دائمًا pending. التغييرات الحقيقية تصل عبر الويب هوك. |
UQ… على الشبكة الرئيسية، 0Q… على شبكة الاختبار). تحويله أو تجميله أو استبداله بترميز آخر لنفس العنوان سيجعل العملات المُرسَلة إلى محفظة لم تُنشر بعد ترتدّ إلى المُرسِل.قراءة فاتورة#
GET /api/merchant/v1/invoices/{invoice_id}
/api/merchant/v1/invoices/{invoice_id}الشكل نفسه أعلاه، مع عكس status وpaid وpaid_minor للحالة الراهنة: paid هو ما وصل بالوحدات العادية، وpaid_minor هو المبلغ نفسه عددًا صحيحًا بالوحدة الصغرى. مفيد كخيار احتياطي عند فقدان ويب هوك، أو في صفحة شكر.
استعلم عنها كحد أقصى كل بضع ثوانٍ، واعتبر الويب هوك القناة الأساسية. الفواتير التي تخص متجرًا آخر تُجيب بـ404 — لا 403، بحيث لا يمكن سبر معرّف للتحقق من وجوده.
إلغاء فاتورة#
POST /api/merchant/v1/invoices/{invoice_id}/cancel
/api/merchant/v1/invoices/{invoice_id}/cancelيُغلق فاتورة ما تزال مفتوحة — pending أو underpaid — ويُحرّر عنوانها. استخدمه عندما يتخلى العميل عن الدفع — العناوين مورد محدود، وإعادتها تُبقي المجمّع بحالة جيدة.
الفاتورة التي لم تعد مفتوحة تُجيب بـ409. وإلغاء فاتورة underpaid لا يعيد العملات إلى أحد: المال المُقيَّد يبقى في رصيدك، وكل ما يُغلق هو قبول الدفعة المكمّلة.
الويب هوك#
ما الذي يصل، وكيفية التحقق منه.
حدّد عنوان الويب هوك عند إنشاء المفتاح. نرسل إليه طلب POST عندما يُقيَّد الدفع — وكذلك عندما يُرفض إيداع احتُجز لفحص إضافي. كل عملية تسليم موقَّعة، ونواصل إعادة المحاولة نحو يوم ونصف حتى تُجيب بـ2xx. سلّم البضاعة عند status: paid أو overpaid، لا لمجرد وصول الاستدعاء.
الأحداث
| الحدث | متى | ما الذي يحمله الجسم |
|---|---|---|
| payment.credited | تأكّد التحويل على السلسلة، واقتُطعت عمولتنا، والباقي في رصيدك. | الحقول المذكورة أدناه. |
| payment.rejected | إيداع احتُجز لفحص إضافي (انظر مرجع الحالات) جرى رفضه. المال لن يصل إلى رصيدك. | invoice_id وorder_id وasset وamount وtx_hash وreason. لا تسلّم البضاعة؛ وإذا كانت الفاتورة paid بالفعل بتحويل أسبق، فهذا الحدث يخصّ الإيداع الزائد لا تلك الدفعة. |
ما الذي يصل
{
"event_id": "99f74f58-efbb-4af1-b0a3-76b0073f9e6b",
"type": "payment.credited",
"data": {
"invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
"order_id": "order-1042",
"asset": "USDT_TON",
"amount": "5000000",
"credited": "4905000",
"fee": "95000",
"status": "paid",
"paid_minor": "5000000",
"tx_hash": "97a1f0…"
}
}{
"event_id": "0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
"type": "payment.rejected",
"data": {
"invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
"order_id": "order-1042",
"asset": "USDT_TON",
"amount": "5000000",
"tx_hash": "97a1f0…",
"reason": "could not be matched to any order"
}
}تخطيط الحقول
| الحقل | المعنى |
|---|---|
| event_id | فريد لكل حدث؛ ويأتي أيضًا في ترويسة X-Paysell-Event-Id. احفظه وتجاهل التكرارات — انظر أدناه. |
| data.order_id | مرجعك. ابحث عن طلبك به. |
| data.amount | ما أرسله المشتري في هذا التحويل، بالوحدة الصغرى — بخلاف الواجهة البرمجية التي تستقبل وحدات عادية. |
| data.fee | ما اقتطعناه، بالوحدة الصغرى. |
| data.credited | ما وصل إلى رصيدك: amount − fee، بالوحدة الصغرى. |
| data.paid_minor | إجمالي ما استُلم على هذه الفاتورة حتى الآن، بالوحدة الصغرى. وهو الحقل المهم عند underpaid: الحالة تقول إن ما وصل أقل، وهذا الحقل يقول كم أقل. |
| data.asset | العملة التي وصلت فعلًا. ليست بالضرورة العملة التي طلبتها الفاتورة. |
| data.asset_mismatch | يظهر، وقيمته true، فقط عندما تكون العملة الواصلة غير عملة الفاتورة. تُقيَّد الأموال لك، لكن الفاتورة تبقى غير مدفوعة ولن يصبح status أبدًا paid. |
| data.invoice_asset | يأتي مع asset_mismatch: العملة التي تطلبها الفاتورة فعلًا. |
| data.status | حالة الفاتورة الآن: pending أو underpaid أو paid أو overpaid أو expired. قارنها بما توقعته. |
| data.tx_hash | المعاملة على السلسلة، لسجلاتك وللدعم. |
الترويسات في كل عملية تسليم
X-Paysell-Event: payment.credited
X-Paysell-Event-Id: 99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp: 1789000000
X-Paysell-Signature: sha256=6f1c0e6a…| الترويسة | المعنى |
|---|---|
| X-Paysell-Event | نوع الحدث: payment.credited أو payment.rejected. |
| X-Paysell-Event-Id | فريد لكل حدث. هذه هي القيمة التي تُبنى عليها إزالة التكرار. |
| X-Paysell-Timestamp | لحظة توقيعنا، بثواني يونكس. وهي جزء من النص الموقَّع. |
| X-Paysell-Signature | sha256= يتبعها HMAC بصيغة hex. انظر أدناه. |
التحقق من التوقيع
يُوقَّع كل طلب بسر الويب هوك الذي يُعرض مرة واحدة عند إنشاء المفتاح. التوقيع هو HMAC-SHA256(secret, "{timestamp}.{raw_body}") — الطابع الزمني من X-Paysell-Timestamp، ثم نقطة حرفية، ثم بايتات الجسم. تحقق منه قبل التصرف: بدون ذلك يستطيع أي شخص يعرف رابطك أن يُسلّمك طلبًا مدفوعًا.
Python:
import hmac, hashlib, time
def is_ours(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300: # ±5 minutes
return False
signed = timestamp.encode() + b"." + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
# compare_digest, not ==: a plain comparison leaks the answer through timing
return hmac.compare_digest("sha256=" + expected, signature)Node.js:
const crypto = require("node:crypto")
function isOurs(body, signature, timestamp, secret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false // ±5 min
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(timestamp + ".").update(body) // body is the raw Buffer, not a parsed object
.digest("hex")
const a = Buffer.from(expected), b = Buffer.from(signature)
// timingSafeEqual throws when the lengths differ, so check that first
return a.length === b.length && crypto.timingSafeEqual(a, b)
}وقّع بايتات الجسم الخام، تمامًا كما وصلت. إذا حللت JSON ثم أعدت تسلسله تغيّرت البايتات — ترتيب المفاتيح، المسافات — ولن يتطابق التوقيع. قارن في زمن ثابت (hmac.compare_digest، crypto.timingSafeEqual): المقارنة العادية == تعود أسرع عند اختلاف البايت الأول، وهذا الفارق وحده يكفي لتخمين التوقيع بايتًا بايتًا.
نافذة الطابع الزمني
ارفض كل ما يبتعد طابعه الزمني أكثر من خمس دقائق عن ساعتك أنت، في أي من الاتجاهين. الطابع الزمني موجود داخل النص الموقَّع تحديدًا كي لا يمكن تعديله دون كسر التوقيع؛ والنافذة هي ما يحوّل ذلك إلى حماية. بدونها يبقى الطلب الذي التُقط مرة واحدة صالحًا إلى الأبد ويمكن إعادة تشغيله في أي وقت — فالتوقيع وحده لا تنتهي صلاحيته أبدًا. أبقِ ساعة خادمك مضبوطة على NTP، وإلا بدأ هذا الفحص يرفض عمليات تسليم سليمة.
التكرارات
قد يصل الحدث نفسه أكثر من مرة. هذا ليس خللًا: نُعيد المحاولة حتى تُجيب بـ2xx، وعملية تسليم نجحت لكن استجابتها لم تصلنا تُرسَل مجددًا. سجّل X-Paysell-Event-Id (ويأتي أيضًا باسم event_id في الجسم) واجعل الوصول الثاني بلا أي أثر.
إعادة المحاولات
تنطلق المحاولة الأولى فور تقييد الدفع. وإذا أخفقت — مهلة منتهية، أو رفض اتصال، أو خطأ TLS، أو إعادة توجيه، أو أي حالة خارج 2xx — نُعيد المحاولة وفق جدول ثابت:
دقيقة واحدة → 5 دقائق → 15 دقيقة → ساعة واحدة → 6 ساعات → 24 ساعةسبع محاولات في المجموع، موزّعة على نحو 31 ساعة. المحاولات الأولى متقاربة لأن السبب المعتاد مستقبِل كان يُعاد تشغيله وقد عاد بالفعل؛ والأخيرة متباعدة لأن الطَّرق المتواصل على خادم متوقف منذ يوم لا ينفع أحدًا.
بعد المحاولة الأخيرة تُوسَم عملية التسليم بـdropped ونتوقف من تلقاء أنفسنا. ولا يضيع شيء: يعرض سطر الدفعة في لوحة حسابك الحالة وعدد المحاولات وصنف الخطأ، مع زر إرسال مرة أخرى يبدأ دورة جديدة من المحاولات السبع كلها. وملاذك الآخر هو GET /api/merchant/v1/invoices/{invoice_id} — فالفاتورة تعرف حالتها دائمًا.
كيف ينبغي أن يبدو عنوان الويب هوك
يُفحص العنوان عند حفظه، ثم يُفحص مجددًا قبل كل عملية تسليم. العنوان الذي لا يجتاز الفحص يُجاب عليه بـ422 مع code: "webhook_url_rejected" وقت الحفظ، ويُوسَم التسليم بـfailed — دون أي إعادة محاولة — إن بدأ يخفق لاحقًا. القواعد:
- `https://` فقط، والمنفذ 443. يحمل الويب هوك تفاصيل الدفع؛ وعبر http العادي يقرؤها أي أحد على الطريق.
- اسم نطاق، لا عنوان IP. أنت بحاجة إلى شهادة على أي حال، والشهادات لا تُصدَر لعناوين IP مجرّدة.
- لا `localhost`، ولا اسم
.localأو.internalأو.corpأو.lanأو.test— خوادمنا لا تصل إلى شبكتك، والاسم الذي يُحَل داخل شبكتنا هو بالضبط ما يجب ألا نستدعيه. - لا بيانات اعتماد في العنوان (
https://user:pass@…). ضع رمزك الخاص في المسار أو في معامل استعلام إن احتجت إلى واحد. - كل عنوان يُحَل إليه الاسم يجب أن يكون عامًا — A وAAAA معًا. تُرفض النطاقات الخاصة وloopback وlink-local وCGNAT، ويتكرر الفحص قبل كل تسليم، فتوجيه السجل لاحقًا إلى
127.0.0.1لن ينفع هو الآخر. - إعادة التوجيه إخفاق، لا قفزة. نحن لا نتبعها: العنوان الذي أعطيتنا إياه هو ما فُحص، أما الذي في ترويسة
Locationفلم يُفحص.
أجب بسرعة
يكفي أي 2xx، خلال عشر ثوانٍ — تلك هي مهلتنا كاملةً، بما فيها إنشاء الاتصال. أجب أولًا ثم قم بالعمل البطيء بعد ذلك؛ فالمستقبِل الذي ينتظر قاعدة بياناته قبل الرد سيُسجَّل عاجلًا أو آجلًا كمهلة منتهية وتُعاد المحاولة إليه، فتعالج الحدث نفسه مرتين. وأي شيء آخر — 4xx أو 5xx أو إعادة توجيه أو تعليق — يُحتسب محاولة فاشلة ويعود إلى الجدول أعلاه.
عن التسليم، بصراحة
المضمون هو آلية التسليم: سبع محاولات على مدى نحو 31 ساعة، وإعادة إرسال يدوية من لوحة حسابك، ونقطة نهاية للفاتورة تعرف الحالة الحقيقية دائمًا. ابنِ تدفقك بحيث لا يكلّفك ويب هوك لم يصل أبدًا شيئًا — اقرأ الفاتورة في صفحة الشكر، أو طابِق الفواتير المفتوحة مرة كل ساعة. الويب هوك هو المسار السريع، لا المسار الوحيد.
A complete receiver#
Signature, deduplication and a fast answer, end to end.
The snippets above verify one signature. This is the whole endpoint: raw body, signature check, deduplication by event_id, a fast 2xx, and the one condition that is allowed to mark an order paid.
Node.js with Express. express.raw is the part people get wrong: express.json() hands you a parsed object, and bytes you re-serialise from it are not the bytes we signed.
const express = require("express")
const crypto = require("node:crypto")
const app = express()
const SECRET = process.env.PAYSELL_WEBHOOK_SECRET
function isOurs(body, signature, timestamp) {
const sentAt = Number(timestamp)
if (!Number.isFinite(sentAt)) return false
if (Math.abs(Date.now() / 1000 - sentAt) > 300) return false // ±5 minutes
const expected = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(timestamp + ".").update(body) // raw Buffer, not a parsed object
.digest("hex")
const a = Buffer.from(expected), b = Buffer.from(signature ?? "")
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
app.post(
"/paysell/webhook",
express.raw({ type: "application/json" }), // NOT express.json()
async (req, res) => {
const signature = req.get("X-Paysell-Signature")
const timestamp = req.get("X-Paysell-Timestamp")
if (!isOurs(req.body, signature, timestamp)) return res.sendStatus(401)
const event = JSON.parse(req.body.toString("utf8"))
// Answer first: 10 seconds is the whole timeout, connection included.
res.sendStatus(200)
// Deduplicate. In real code this is a unique column, not a Set.
if (await alreadyHandled(event.event_id)) return
await remember(event.event_id)
if (event.type !== "payment.credited") return
const { order_id, status, credited, asset, tx_hash } = event.data
// The only condition that may release the goods.
if (status !== "paid" && status !== "overpaid") return
await markOrderPaid(order_id, { credited, asset, tx_hash })
}
)Python with Flask. request.get_data() is the raw body; request.form and request.json are not.
import hashlib
import hmac
import json
import os
import time
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["PAYSELL_WEBHOOK_SECRET"]
def is_ours(body: bytes, signature: str, timestamp: str) -> bool:
try:
sent_at = int(timestamp)
except (TypeError, ValueError):
return False
if abs(time.time() - sent_at) > 300: # ±5 minutes
return False
signed = timestamp.encode() + b"." + body
expected = hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + expected, signature)
@app.post("/paysell/webhook")
def paysell_webhook():
body = request.get_data() # raw bytes, unparsed
if not is_ours(body, request.headers.get("X-Paysell-Signature", ""),
request.headers.get("X-Paysell-Timestamp", "")):
return "", 401
event = json.loads(body)
# Deduplicate. In real code this is a unique column, not a set.
if already_handled(event["event_id"]):
return "", 200 # a repeat is still a success
remember(event["event_id"])
if event["type"] == "payment.credited":
data = event["data"]
# The only condition that may release the goods.
if data["status"] in ("paid", "overpaid"):
mark_order_paid(data["order_id"], data)
return "", 200 # 2xx within 10 secondsWhat the code is doing, and why
- Verify before anything acts on the body. An unsigned request that reaches your business logic is a paid order for whoever found your URL.
- Answer 2xx first, work afterwards. Ten seconds is the whole timeout, connection included. A handler that waits for its own database gets recorded as a timeout and retried, and you process the same event twice.
- Deduplicate on `event_id` in storage that survives a restart. The in-memory set in the examples keeps them short; a real one is a unique column in your database.
- Mark the order paid only on `status: paid` or `overpaid`.
underpaidmeans part of the money arrived and the invoice is still open, and a deposit in the wrong coin never makes an invoice paid either. - Answer 2xx to a duplicate too. A repeat that gets a 4xx looks like a failure to us and comes back again on the schedule.
payment.rejected arrives at the same endpoint. It means a deposit held for an additional check was declined and the money will not be credited: release nothing, and if the invoice was already paid by an earlier transfer, this event is about the extra deposit, not about that payment.
مرجع الحالات#
جميع حالات الفواتير والمدفوعات، موضّحة.
الفاتورة
| الحالة | المعنى | ما ينبغي فعله |
|---|---|---|
| pending | بانتظار الدفع. | أبقِ الطلب مفتوحًا. |
| paid | مدفوعة بالكامل. | سلّم البضاعة. |
| overpaid | وصل أكثر من المطلوب. يُقيَّد الفائض لك بالكامل. | سلّم البضاعة؛ ورُدّ الفرق إن رغبت. |
| underpaid | وصل أقل من المطلوب. تبقى الفاتورة مفتوحة وتحتفظ بعنوانها: يستطيع المشتري إتمام الدفع إلى المكان نفسه، ويقول paid_minor كم وصل حتى الآن. وتظل قابلة للدفع بقية عمرها زائد مهلة سماح مدتها 24 ساعة بعد expires_at. | انتظر الدفعة المكمّلة، أو اتفق مع العميل. لا تسلّم البضاعة — الفاتورة غير مدفوعة. |
| expired | أُغلقت النافذة، بما في ذلك مهلة السماح. وقد تحمل مالًا رغم ذلك: كل ما وصل بقي في رصيدك، ويقول paid_minor كم. | قدّم فاتورة جديدة. لا تقبل الدفع على العنوان القديم: فبمجرد انتهاء صلاحية الفاتورة يعود العنوان إلى المجمّع، والتحويل المتأخر جدًا حالة تُعالَج عبر الدعم لا قيدًا تلقائيًا. راجع paid_minor قبل أن تخبر العميل بأن شيئًا لم يصل. |
| cancelled | ألغيتها أنت. يعود العنوان إلى المجمّع. | لا شيء. |
الدفعة
مرئية في لوحة حسابك؛ مفيدة عند دعم عميل أثناء الدفع.
| الحالة | المعنى |
|---|---|
| detected | شوهدت على السلسلة، بانتظار التأكيدات. |
| confirmed | أكدتها الشبكة. الإضافة إلى الرصيد تالية. |
| credited | في رصيدك. هذه هي اللحظة التي يُطلَق فيها الويب هوك. |
| review | محتجزة لفحص إضافي — كوصول عملات إلى عنوان بلا فاتورة مفتوحة، على سبيل المثال. |
| rejected | لم تُضَف إلى الرصيد. السبب مُسجَّل. |
عندما تذهب الدفعة إلى `review`
بعض الإيداعات تُحتجَز لفحص إضافي بدل تقييدها فورًا: مبلغ كبير على غير العادة، أو عملات تصل إلى عنوان بلا فاتورة مفتوحة، أو اختلاف بين مصدري بيانات البلوكتشين اللذين نستعلمهما حول ما جرى. لا يضيع شيء — المال ينتظر القرار، ويُطلَق الويب هوك فور صدوره، وقد يكون ذلك بعد دقائق أو ساعات. فتعامل مع غياب الاستدعاء في دفعة معروضة بحالة review على أنه أمر طبيعي لا خلل. وإن كان ذلك مهمًا لطلب ما، فاسأل الدعم واذكر tx_hash.
المبالغ#
إلى الخارج وحدات عادية، وفي العودة الوحدات الصغرى.
أرسل المبالغ بالوحدات العادية للعملة، كسلسلة نصية — "1.5" تعني واحدًا ونصفًا. لا رقم JSON ولا الوحدة الصغرى.
| الأصل | الخانات العشرية | أنت ترسل | amount_minor في الاستجابة |
|---|---|---|---|
| TON | 9 | "1.5" | "1500000000" |
| USDT_TON | 6 | "1.5" | "1500000" |
سلسلة نصية لا رقمًا، لأن أرقام JSON هي أعداد IEEE-754 مزدوجة الدقة، والمبلغ الكبير بالنانوتون لم يعد يُمثَّل فيها بدقة. عدد خانات عشرية أكبر مما تملكه العملة يعني 422، ولا يعني أبدًا تقريب أموالك في صمت. الويب هوك يسير في الاتجاه المعاكس: فحقول amount وfee وcredited هناك أعداد صحيحة بالوحدة الصغرى، لأن ذلك الجانب يقرأه كود لا إنسان.
// Send amounts in the coin's normal units, as a string:
const amount = "1.5" // one and a half TON or USDT
// In responses, amount is that same human string; amount_minor is the
// integer in smallest units — use it for exact maths, as a string or BigInt:
BigInt(invoice.amount_minor) // e.g. 1500000nالحدود#
الحدود الدنيا والقصوى وحدود المعدل.
| الحد | القيمة | عند التجاوز |
|---|---|---|
| الحد الأدنى للفاتورة | 0.1 TON · 3 USDT | 422 |
| الحد الأقصى للفاتورة | 7000 TON · 10000 USDT | 422 |
| الفواتير في الساعة لكل متجر | 60 | 429 |
| الفواتير المفتوحة في آنٍ واحد | 20، ويزيد مع كل فاتورة مدفوعة حتى 200 | 429 |
| عمر الفاتورة | من دقيقة واحدة إلى 24 ساعة (الافتراضي ساعتان) | 422 |
| طلبات الواجهة البرمجية لكل مفتاح | 120 في الدقيقة | 429 + Retry-After |
الحد الأدنى ليس بيروقراطية. عمولتنا نسبة مئوية، لكن تحصيل دفعة يكلّف مبلغًا ثابتًا: سحب USDT من عنوان استلام يعني تزويده بالغاز أولًا، من جيبنا نحن. ودون بضعة دولارات لا تُغطي العمولة تكلفة المعالجة، وقبول مثل هذه الدفعة يعني أن نُقيّد لك مالًا نقله غير مجدٍ اقتصاديًا.
الحد الأقصى ليس موجّهًا ضد المتاجر الكبيرة — إنه فخ لخطأ في الوحدات. أرسل "5000000" وأنت تقصد "5"، ولولاه لصدرت فاتورة بخمسة ملايين دولار: يرى المشتري مبلغًا سخيفًا فينصرف. الطلب الحقيقي لا يبلغ هذا السقف أبدًا، والخاطئ يبلغه دائمًا. وكلا السقفين إعداد (invoice_max_ton، invoice_max_usdt) ويمكن رفعه لمتجرك — اطلب ذلك.
الحد الساعي وحد الفواتير المفتوحة يحميان معًا مجمّع العناوين. تشغل كل فاتورة مفتوحة عنوان استلام، وحلقة خارجة عن السيطرة على موقع واحد كانت ستستنزف المجمّع على الجميع. يستطيع المتجر الجديد إبقاء 20 فاتورة مفتوحة في آنٍ واحد؛ ويزيد المسموح بواحدة عن كل فاتورة حصّلها فعلًا، حتى سقف 200. وتُحتسَب underpaid مفتوحةً — فهي ما تزال تشغل عنوانها بانتظار الباقي. وإلغاء فاتورة متروكة يعيد عنوانها فورًا. أما إعادة المحاولات بنفس idempotency_key فلا تُحتسَب ضمن الحد الساعي.
حد الطلبات هو 120 في الدقيقة لكل مفتاح واجهة برمجية — أي استدعاءان في الثانية، وهو أعلى بكثير من أي تدفق طلبات حقيقي. ويحمل الرد 429 ترويسة Retry-After بالثواني: انتظر تلك المدة بدل إعادة المحاولة في حلقة متلاحقة، فذلك لا يفعل سوى دفع النافذة أبعد.
الأخطاء#
رموز الحالة التي سترها فعليًا.
تعود الأخطاء بصيغة JSON، وفي شكلين اثنين. كل ما نقرره نحن أو نواة المعالجة يضع زوج {code, message} تحت detail. أما جسم الطلب الذي لا يجتاز التحقق فيضع هناك قائمة بأخطاء الحقول بدلًا من ذلك. تحقق أيّ الشكلين وصلك قبل قراءة detail.code — واتخذ قرارك بناءً على `code`، لا على `message` أبدًا: فالصياغة قد تتغير في أي وقت، أما الرمز فلا.
{
"detail": {
"code": "invalid_input",
"message": "invoice amount below the minimum: 0.010000 USDT_TON, minimum 3.000000 USDT_TON"
}
}{
"detail": [
{
"type": "string_type",
"loc": ["body", "amount"],
"msg": "Input should be a valid string",
"input": 5
}
]
}| الحالة | متى | ما ينبغي فعله |
|---|---|---|
| 401 | المفتاح مفقود أو خاطئ أو مُلغى. | تحقق من الترويسة. أعِد إصدار المفتاح إذا أُلغي. |
| 404 | لا توجد هذه الفاتورة، أو تخص متجرًا آخر. | تحقق من المعرّف. الحالتان تُجابان بالطريقة نفسها عن قصد، كي لا يمكن استكشاف المعرّفات. |
| 409 | الفاتورة في حالة تمنع هذا الإجراء. | اقرأ حالتها الحالية أولًا. |
| 422 | الطلب غير صحيح البنية، أو المبلغ خارج حدود الفاتورة. | تذكر الرسالة القيمة المرسَلة والحد كليهما. |
| 429 | فواتير كثيرة جدًا هذه الساعة، أو مفتوحة كثيرة في آنٍ واحد، أو طلبات كثيرة جدًا. | انتظر انقضاء Retry-After ثم أعد المحاولة. |
| 502 | لم نتمكن من الوصول إلى نواة المعالجة. | أعد المحاولة بمفتاح التطابق (idempotency) نفسه. |
الرموز
الشكل الذي نقرره نحن هو {"detail": {"code": …, "message": …}}. وهذه هي الرموز التي تعيدها واجهة التاجر البرمجية.
| الرمز | الحالة | المعنى |
|---|---|---|
| invalid_api_key | 401 | المفتاح مفقود أو تالف أو غير معروف أو مُلغى. والحالات الأربع تُجيب بالطريقة نفسها، فلا يمكن سبر مفتاح. |
| not_found | 404 | لا يوجد هذا الكائن، أو أنه يخص متجرًا آخر. |
| invalid_input | 422 | لم يجتز الطلب التحقق في النواة — مبلغ غير صالح، أو خانات عشرية زائدة، أو مبلغ خارج حدود الفاتورة. |
| conflict | 409 | الإجراء يناقض الحالة الراهنة، كإلغاء فاتورة لم تعد مفتوحة. |
| too_many_requests | 429 | حد معدل: الفواتير في الساعة، أو الفواتير المفتوحة، أو الطلبات في الدقيقة. وتقول Retry-After كم تنتظر. |
| cbc_unreachable | 502 | تعذّر علينا الوصول إلى نواة المعالجة. أعد المحاولة بنفس idempotency_key. |
| webhook_url_rejected | 422 | عند حفظ المفتاح فقط: لم يجتز عنوان الويب هوك الفحوص أعلاه. ويسمّي detail.reason القاعدة المخالَفة — scheme، port، ip_literal، local_hostname، private_address، dns_error، credentials، وهكذا. |
لا تعني 502 أن الفاتورة لم تُنشأ — فقد يكون الطلب قد اكتمل مع ضياع الاستجابة في طريق العودة. أعد المحاولة بنفس idempotency_key وستحصل إما على الفاتورة الموجودة أو على فاتورة جديدة، لا فاتورتين أبدًا.
Checkout: what the buyer sees#
The hosted payment page, and when to build your own.
payment_url points at https://paysell.me/pay/{invoice_id}. One page, no account, no login, mobile first, and nothing for you to build.
On the page
- Your shop's name, the amount and the coin, large, with the invoice's description underneath.
- A countdown to
expires_at— plus the 24-hour grace period when the invoice isunderpaid. - The invoice's short id with a copy button, so a buyer can quote it to your support.
- Wallet buttons: Tonkeeper and MyTonWallet open with the address and the amount already filled in. Other reveals a QR code and the address with a copy button.
- A warning that only this invoice's coin, on the TON network, may be sent — anything else is lost.
How the page reacts
| Invoice | What the buyer sees |
|---|---|
| pending | “Waiting for payment”, with the wallet choices and the countdown. The page re-reads the invoice every five seconds. |
| underpaid | “Received X of Y”, the exact remainder still owed, and the same address to send it to. The wallet link is prefilled with what is missing, not with the original total — otherwise the buyer would pay twice. |
| paid · overpaid | “Payment received”, and a button back to your shop if the shop has a URL. |
| expired | “Payment window closed”. If money did arrive, the amount is named with a note to contact you — silence here would send the buyer looking for their coins. |
| cancelled | “Payment cancelled”, with a link back to your shop. |
If you build your own
You gain your own branding and take on all of the above: the exact address string, the right coin, the countdown with its grace period, the underpayment case, and polling. GET /api/merchant/v1/public/invoices/{invoice_id} is the same unauthenticated read the hosted page uses — rate limited per IP, so poll it no more often than every few seconds. Print the address exactly as returned.
Typical integration mistakes#
The handful that account for most broken integrations.
None of these are exotic. Every one of them has cost somebody a day.
Sending the amount as a number
{"amount": 5}is a422. It has to be the string"5": JSON numbers are IEEE-754 doubles, and a large sum in nanotons stops being exactly representable in one.Sending the smallest unit
"5000000"for 5 USDT is a mistake in units, and the upper invoice limit exists to catch it. Smallest units are what comes back in webhooks, not what goes out in requests.Treating the webhook's arrival as payment
Read
data.status.underpaidis not paid, and a deposit in a coin the invoice did not ask for never makes it paid either. Release the goods onpaidoroverpaid, on nothing else.Not checking the timestamp
A signature on its own never expires. Without the ±5 minute window on
X-Paysell-Timestamp, a delivery captured once can be replayed at any time and will still verify.Verifying the signature over re-serialised JSON
Parse the body and serialise it again and the bytes change — key order, spacing — and the HMAC no longer matches. Sign the raw bytes exactly as received.
No deduplication
The same
event_idwill arrive twice sooner or later: we retry until you answer 2xx, and a response lost on the way back looks like a failure from here. The second arrival must do nothing.Using the
Idempotency-KeyheaderThis API reads
idempotency_keyfrom the request body; the header is not read at all. A retry without the field opens a second invoice for the same order.Assuming
detailis always an objectIt is
{code, message}for anything we or the core decide, and a list of field errors when the body itself fails validation. Check which one you got before readingdetail.code.
الاسترداد#
كيف تسترد مبلغًا لعميل.
الاسترداد يتم عبر الدعم، لا باستدعاء واجهة برمجية. الاسترداد تحويل جديد إلى عنوان قدّمه شخص ما، ومعالِج مدفوعات يرسل المال عائدًا تلقائيًا عند استدعاء واجهة برمجية هو معالِج يمكن دفعه إلى إرسال المال إلى عنوان مهاجم. لذلك فهو يدوي عن قصد.
لاسترداد مبلغ لمشترٍ، افتح تذكرة دعم من لوحة حسابك تتضمن invoice_id أو tx_hash، والمبلغ، والعنوان الذي تُرسَل إليه الأموال. يتحقق المشغّل من الدفعة، ويسحب المال من رصيدك، ويجيب في التذكرة نفسها. توقّع أن يستغرق ذلك يوم عمل، لا دقيقة.
وهناك نتيجتان يجدر التصميم على أساسهما. الدفع الزائد يُقيَّد لك بالكامل — لا نحتفظ منه بشيء — لذا فإعادة الفرق إلى مشترٍ أرسل أكثر من اللازم قرارك أنت، وتسلك الطريق نفسه. والفاتورة الناقصة الدفع ليست حالة استرداد ما دامت مفتوحة: المال في رصيدك، والعنوان ما يزال مراقَبًا، ويستطيع المشتري ببساطة إتمام الدفع. ولا يظهر قرار يُتخذ إلا بعد مهلة السماح، حين تصير الفاتورة expired ومعها مال.
الاختبار#
كيف تختبر تكاملك قبل الإطلاق.
المفاتيح هنا فعلية: كل مفتاح يُصدَر هو مفتاح sk_live_ يعمل مقابل النواة الإنتاجية وشبكة TON الرئيسية. لا توجد بيئة اختبار منفصلة، وفي ذلك ميزة: أنت تجرّب المسار نفسه الذي ستسلكه طلباتك الحقيقية.
لذا اختبر كما تختبر أي شيء يمس مالًا حقيقيًا: بمبالغ صغيرة. أنشئ فاتورة بالحد الأدنى (0.1 TON أو 3 USDT)، وادفعها من محفظتك أنت، وراقب المسار كاملًا — صفحة الدفع، والويب هوك، والتحقق من التوقيع، وتحوّل طلبك إلى مدفوع. تُطبَّق العمولة، والعملات تتحرك فعلًا.
أما الأجزاء التي يمكنك تجربتها دون إنفاق شيء فهي: إنشاء الفاتورة وقراءتها، وإلغاؤها، و422 على مبلغ غير صحيح البنية، و401 على مفتاح خاطئ، والتحقق من التوقيع لديك — وقّع جسمًا نموذجيًا بسرّك ومرّره إلى معالِجك أنت. والشيء الوحيد الذي يتطلب دفعة حقيقية هو الخطوة الأخيرة فقط: ويب هوك payment.credited فعلي.
خطّط للتكامل بحيث لا يعتمد على بيئة اختبارية أو على محاكاة دفعة: المسار الفعلي يُتحقَّق منه أسرع — وبصورة أصدق.
For AI agents and LLMs#
Machine-readable copies of this page, and a prompt to start from.
Everything on this page also exists in a form a model can read directly. Point your assistant at one of these instead of pasting screenshots of documentation into a chat.
The three files
| File | What it is | Use it for |
|---|---|---|
| /llms-full.txt | The whole documentation as one markdown file: endpoints, fields, statuses, limits, errors, webhooks with working verification code, the fee, the checkout page, the checklist. | Pasting into a model's context, or letting an agent fetch it. Start here. |
| /llms.txt | A short index in the llms.txt format: what Paysell is, the five rules that decide whether an integration works, and links to everything else. | Letting an agent discover the rest on its own. |
| /openapi.json | OpenAPI 3.1, generated from the running application's own models, both webhook events included. | Generating a client, or loading into anything that speaks OpenAPI. |
A prompt to start from
Copy this, replace the stack, and hand it to your assistant. It names the four things that go wrong most often, so the answer does not have to be corrected afterwards.
Read https://paysell.me/llms-full.txt and implement Paysell payments in my <stack>:
create invoices (POST /api/merchant/v1/invoices, Bearer sk_live_ key, amount as a
decimal string in normal units), redirect the buyer to payment_url, verify webhook
signatures (HMAC-SHA256 over "{timestamp}.{raw_body}", header X-Paysell-Signature,
reject anything whose X-Paysell-Timestamp is more than 300 seconds off), deduplicate
by event_id, answer 2xx within 10 seconds, and mark orders paid only on a
payment.credited event whose data.status is "paid" or "overpaid".Feeding it to a specific tool
- Agents with web access — Claude Code, Cursor, Windsurf and the like: give them the
/llms-full.txtlink. One fetch, no setup. - A chat window — ChatGPT, Claude, Gemini: paste the contents of
/llms-full.txtinto the conversation or attach it as a file. It is written to fit in one message. - OpenAPI tooling — client generators, Postman, an agent's tool schema: point it at
https://paysell.me/openapi.json. Itsserversentry already carries the production base URL, so generated calls go to the right place.
قائمة تحقق ما قبل الإطلاق#
عشرة أمور تُراجَع قبل الإطلاق.
- المفتاح على جانب الخادم فقط، أبدًا في جافاسكريبت المتصفح.
- يُتحقَّق من توقيع الويب هوك مقابل
"{timestamp}.{raw_body}"، وفي زمن ثابت. - تُرفض عمليات التسليم الأقدم من خمس دقائق، وساعة الخادم مضبوطة على NTP.
- لا يفعل
X-Paysell-Event-Idالمتكرر شيئًا في المرة الثانية. - يجيب الويب هوك بـ2xx خلال عشر ثوانٍ؛ ويحدث العمل البطيء بعد ذلك.
- عنوان الويب هوك نطاق على https:// وعلى المنفذ 443، دون إعادة توجيه أمامه.
- الويب هوك الضائع أمر يمكن تجاوزه: تُقرأ نقطة نهاية الفاتورة في صفحة الشكر أو ضمن مسح مطابقة دوري.
- يُولَّد
idempotency_keyمرة واحدة لكل طلب ويُعاد استخدامه عند إعادة المحاولة. - تخرج المبالغ بالوحدات العادية كسلاسل نصية؛ وتُقرأ أرقام الويب هوك بالوحدة الصغرى.
- يُعرض العنوان تمامًا كما أُعيد، دون تعديل.
- تُعالَج
overpaidوunderpaid، لاpaidفقط؛ وقد تحملexpiredقيمة فيpaid_minor. - تُسلَّم البضاعة عند
status: paidأوoverpaid، ولا تُسلَّم أبدًا لمجرد وصول الاستدعاء. - تُعالَج
429بانتظار انقضاءRetry-After، لا بإعادة المحاولة فورًا. - تُقرَأ الأرصدة منّا، ولا تُتبَّع بشكل منفصل باعتبارها الحقيقة.
هل هناك شيء غير واضح؟
إذا لم تُجب هذه الصفحة عن سؤالك، فهذه ثغرة في التوثيق تستحق إخبارنا بها. راسلنا من لوحة حسابك وسنُصلح الصفحة، لا الإجابة فقط.