REST API - v1
واجهة برمجية لشحن الألعاب والبطاقات للموزّعين
واجهة REST واحدة لشحن الألعاب وأكواد البطاقات: اقرأ الكتالوج، وتحقّق من اللاعب، وأنشئ الطلب، واستقبل النتيجة عبر webhook أو باستعلام الحالة.
هذه هي واجهة الشحن المباشر بين الخوادم (H2H) التي تقف خلف SHOP2TOPUP. إنها واجهة JSON عادية فوق HTTPS مع مصادقة Bearer، ومفاتيح idempotency بصيغة UUID، وحدود معدل معلنة، وأكواد أخطاء ثابتة يقرأها الحاسوب، فيستطيع أي متجر أو واجهة بيع أو بوت أن يبيع رصيد الألعاب الرقمي برمجيًا دون مكتبة عميل. وقد حملت الواجهة نفسها أكثر من 3,000,000 طلب مُسلَّم على مدى أكثر من 5 سنوات من التشغيل المتواصل.
تبحث عن الأسعار ووسائل الدفع وكيفية عمل حساب الشريك نفسه؟ اقرأ نظرة عامة على برنامج موزّعي SHOP2TOPUP.
نداؤك الأول، قبل أن توقّع أي شيء
كل نقطة نهاية هي مجرد طلب HTTPS عادي يحمل رأسًا واحدًا. لا SDK لتثبيته، ولا مكتبة عميل لإدراجها، ولا خطوة بناء لإضافتها. الصق النداء أدناه، وضع مفتاحك، لتحصل على استجابة كتالوج حيّة في طرفيّتك.
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
-H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"{
"success": true,
"categories": [
{
"id": 12,
"name": "Free Fire",
"description": "Garena Free Fire diamonds",
"big_category_id": 1,
"big_category_name": "Mobile Games"
}
]
}العنوان الأساسي
https://shop2topup.com/api/endpoints/v1
كل مسار موثّق هنا نسبيّ إلى هذا العنوان الأساسي. الاستجابات بصيغة JSON وتحمل دائمًا حقلًا منطقيًا اسمه success، فيكفي شرط واحد في عميلك للفصل بين المسار الناجح وكل ما عداه.
مغلّف الخطأ
تحتفظ الإخفاقات بالشكل نفسه عند كل رمز حالة. تفرّع على error.code فهو جزء من العقد؛ ولا تتفرّع أبدًا على error.message المكتوب للبشر والقابل لإعادة الصياغة في أي وقت.
{
"success": false,
"error": {
"code": "PRICE_INCREASED",
"message": "Price has increased beyond expected",
"action": "FIX_INPUT",
"retryable": false,
"retry_after": null,
"details": {
"expected_unit_price": "0.950000",
"current_unit_price": "0.980000"
}
}
}سطح الواجهة البرمجية كاملًا في شاشة واحدة
إحدى عشرة نقطة نهاية تغطي التكامل بأكمله: اقرأ الكتالوج، وسعّر عنصرًا، وتحقّق من لاعب، وأنشئ طلبًا، ثم سوِّه محاسبيًا بعد ذلك. كل صفّ يقود مباشرةً إلى المرجع الكامل، بكل معاملاته ومثال طلب ومثال استجابة.
| الطريقة | المسار | ماذا تفعل | المرجع |
|---|---|---|---|
| GET | /account | جلب معلومات الحساب | اقرأ المرجع |
| GET | /catalog/big-categories | عرض الفئات الكبرى | اقرأ المرجع |
| GET | /catalog/categories | عرض الفئات | اقرأ المرجع |
| GET | /catalog/subcategories | عرض الفئات الفرعية (المنتجات) | اقرأ المرجع |
| GET | /catalog/subcategory/:itemId/price | جلب سعر المنتج | اقرأ المرجع |
| GET | /catalog/category/:categoryId/requirements | جلب متطلبات الفئة | اقرأ المرجع |
| POST | /player/validate | التحقق من اللاعب | اقرأ المرجع |
| POST | /orders/create | إنشاء طلب | اقرأ المرجع |
| GET | /orders/:orderId | جلب حالة الطلب | اقرأ المرجع |
| POST | /orders/batch | جلب الطلبات دفعة واحدة | اقرأ المرجع |
| GET | /orders | قائمة الطلبات | اقرأ المرجع |
إضافةً إلى خطافات ويب لحالة الطلب، موثّقة بالكامل أسفل هذه الصفحة.
المصادقة والتحكّم في الوصول
المصادقة رأس HTTP واحد. يحمل كل طلب رأس Authorization يتضمّن بيان اعتماد Bearer مكوَّنًا من معرّف مفتاح وسرّ، يُصدران من صفحة API Access في لوحة الموزّع. معرّف المفتاح يحدّد الحساب؛ والسرّ يثبت ملكيتك له ويُتحقق منه على الخادم بفحص HMAC.
يُعرض السرّ مرة واحدة فقط، عند الإنشاء، ولا يمكن استرجاعه بعدها أبدًا. ضعه في مدير الأسرار الذي تستخدمه أصلًا، ومرّره كمتغيّر بيئة، وأبقِه خارج مستودع الكود. وإن فقدته فأنت تُدوّر مفتاحًا جديدًا لا تسترجع القديم.
لا يوجد تحويل OAuth، ولا كوكي جلسة، ولا رمز تحديث، ولا شيء يحتاج إلى جدولة. وهذا مهمّ للتكاملات الآلية: تستطيع مهمة cron أو عامل طابور أن يحتفظ ببيان الاعتماد نفسه لسنوات دون مسار تجديد، ولا يوجد مؤقّت انتهاء يوقظك في الثالثة فجرًا.
يمكن إرفاق قائمة عناوين IP مسموح بها بالمفتاح. وبمجرد أن تصبح غير فارغة، تُرفض الطلبات من أي عنوان آخر بالكود IP_NOT_ALLOWED و HTTP 403 قبل تشغيل أي منطق عمل، فيصبح بيان الاعتماد المسرَّب عديم الفائدة خارج خوادمك. والتدوير عملية نشر لا ترحيل: أنشئ المفتاح الجديد، وانشره، ثم احذف القديم.
المفاتيح تعود إلى حساب موزّع واحد وتُنفق من محفظة ذلك الحساب، لذا لا ترسل السرّ أبدًا إلى متصفّح أو حزمة تطبيق جوال أو مستودع عام. نادِ الواجهة البرمجية من خادمك الخلفي ودع واجهتك الأمامية تتحدث إلى خادمك. وكل ما هو موثّق هنا يفترض مناديًا من خادم إلى خادم.
Authorization: Bearer <keyId>.<secret>تأكّد من المفتاح بنداء واحد
GET /account هي فحص السلامة لبيان الاعتماد. تجيب عن ثلاثة أسئلة دفعةً واحدة: هل يصادق المفتاح، وهل الحساب مفعّل، وهل تكفي المحفظة لتغطية ما توشك أن تطلبه. شغّلها عند الإقلاع وقبل أي تشغيل دفعي.
اقرأ مرجع المصادقة والحسابتكامل كامل من البداية إلى النهاية
التكامل العامل أربع خطوات. تنفّذ الأمثلة أدناه هذه الخطوات جميعها على الواجهة الحيّة بلغة cURL و Node و PHP. ضع بيان اعتمادك مكانها وستعمل كما هي.
- 1
اقرأ الكتالوج
انتقل من الفئات الكبرى إلى الفئات ثم إلى الفئات الفرعية، ثم اقرأ السعر الحالي للعنصر الذي توشك أن تبيعه. خزّن الشجرة مؤقتًا؛ وسعّر العنصر لحظيًا.
- 2
تحقّق من اللاعب
حلّ المعرّف إلى اسم داخل اللعبة وإلى منطقة قبل تحرّك أي مال. هنا تُلتقط الأخطاء المطبعية وأخطاء المناطق.
- 3
أنشئ الطلب
ولّد UUID، واحفظه، ثم أرسله بوصفه order_id. هو مفتاح idempotency الخاص بك، وهو الطريقة الآمنة الوحيدة لإعادة محاولة طلب إنشاء.
- 4
أكّد التسليم
انتظر خطاف الويب الخاص بحالة الطلب، وارجع إلى قراءة الحالة لأي طلب ما يزال pending بعد نافذة المهلة التي تحدّدها.
cURL - التدفق الكاملإظهار الكودإخفاء الكود
export S2T_KEY="YOUR_KEY_ID.YOUR_KEY_SECRET"
export S2T_BASE="https://shop2topup.com/api/endpoints/v1"
# 1. Find the item you want to sell.
curl -s "$S2T_BASE/catalog/subcategories?categoryId=12" \
-H "Authorization: Bearer $S2T_KEY"
# 2. Read the exact price you will be charged for it.
curl -s "$S2T_BASE/catalog/subcategory/999/price" \
-H "Authorization: Bearer $S2T_KEY"
# 3. Confirm the player exists BEFORE any money moves.
curl -s -X POST "$S2T_BASE/player/validate" \
-H "Authorization: Bearer $S2T_KEY" \
-H "Content-Type: application/json" \
-d '{"sub_category_id": 999, "player_id": "123456789", "server": "Asia"}'
# 4. Create the order. order_id is YOUR uuid and YOUR idempotency key.
curl -s -X POST "$S2T_BASE/orders/create" \
-H "Authorization: Bearer $S2T_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "01912345-6789-7abc-8def-0123456789ab",
"sub_category_id": 999,
"quantity": 1,
"requirements": { "player_id": "123456789", "server": "Asia" },
"expected_unit_price": "0.950000"
}'
# 5. Read the order back until it leaves "pending".
curl -s "$S2T_BASE/orders/01912345-6789-7abc-8def-0123456789ab" \
-H "Authorization: Bearer $S2T_KEY"Node.js - التدفق الكاملإظهار الكودإخفاء الكود
// Node 18+ — no dependencies, global fetch and global crypto.
const BASE = 'https://shop2topup.com/api/endpoints/v1';
const KEY = process.env.S2T_KEY; // "<keyId>.<secret>", server-side only.
async function call(path, init = {}) {
const res = await fetch(BASE + path, {
...init,
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
...(init.headers || {}),
},
});
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') || 5);
await new Promise((r) => setTimeout(r, wait * 1000));
return call(path, init);
}
const body = await res.json();
if (!res.ok || body.success === false) {
throw Object.assign(new Error(body?.error?.code || 'HTTP_' + res.status), {
code: body?.error?.code,
status: res.status,
});
}
return body;
}
async function sell({ itemId, categoryId, playerId, server }) {
// 1 + 2. Catalog and current price.
const { subcategories } = await call(
`/catalog/subcategories?categoryId=${categoryId}`,
);
const item = subcategories.find((s) => s.item_id === itemId);
const { price } = await call(`/catalog/subcategory/${itemId}/price`);
// 3. Player check before charging.
const { player } = await call('/player/validate', {
method: 'POST',
body: JSON.stringify({
sub_category_id: itemId,
player_id: playerId,
server,
}),
});
// 4. Create the order. Persist orderId BEFORE the call so a crash mid-flight
// can be replayed with the same uuid instead of double-charging.
const orderId = crypto.randomUUID();
await saveIntent(orderId, itemId, playerId);
const { order } = await call('/orders/create', {
method: 'POST',
body: JSON.stringify({
order_id: orderId,
sub_category_id: itemId,
quantity: 1,
requirements: { player_id: playerId, server },
expected_unit_price: price.unit_price,
}),
});
// 5. Confirm. A webhook usually beats the poll; the poll is the safety net.
let status = order.status;
while (status === 'pending') {
await new Promise((r) => setTimeout(r, 20000));
status = (await call(`/orders/${orderId}`)).order.status;
}
return { orderId, status, itemName: item?.name, playerName: player.player_name };
}PHP - التدفق الكاملإظهار الكودإخفاء الكود
<?php
// PHP 8 — plain cURL, no SDK required.
const S2T_BASE = 'https://shop2topup.com/api/endpoints/v1';
function s2t(string $path, ?array $body = null): array
{
$ch = curl_init(S2T_BASE . $path);
$headers = [
'Authorization: Bearer ' . getenv('S2T_KEY'),
'Content-Type: application/json',
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true) ?: [];
if ($status >= 400 || ($data['success'] ?? false) === false) {
throw new RuntimeException($data['error']['code'] ?? 'HTTP_' . $status);
}
return $data;
}
// 1 + 2. Catalog, then the exact price for the item.
$catalog = s2t('/catalog/subcategories?categoryId=12');
$price = s2t('/catalog/subcategory/999/price');
// 3. Player check before charging.
$player = s2t('/player/validate', [
'sub_category_id' => 999,
'player_id' => '123456789',
'server' => 'Asia',
]);
// 4. Create the order with your own uuid as the idempotency key.
$orderId = sprintf(
'%04x%04x-%04x-7%03x-%04x-%04x%04x%04x',
random_int(0, 0xffff), random_int(0, 0xffff),
random_int(0, 0xffff), random_int(0, 0x0fff),
random_int(0x8000, 0xbfff),
random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
);
$order = s2t('/orders/create', [
'order_id' => $orderId,
'sub_category_id' => 999,
'quantity' => 1,
'requirements' => ['player_id' => '123456789', 'server' => 'Asia'],
'expected_unit_price' => $price['price']['unit_price'],
]);
// 5. Read it back until it settles.
do {
sleep(20);
$latest = s2t('/orders/' . $orderId);
} while ($latest['order']['status'] === 'pending');
echo $latest['order']['status'];تفصيلان يستحقّان النسخ حرفيًا: احفظ UUID الطلب قبل نداء الإنشاء لا بعده، وأعد قراءة السعر بدل الوثوق برقم مخزَّن. هاتان العادتان تزيلان تقريبًا كل خصم مزدوج وكل رفض بالكود PRICE_INCREASED.
خطافات الويب: نداءات حالة الطلب
سجّل نقطة HTTPS واحدة لتدفع المنصة نتائج الطلبات إليها فور حدوثها، فيتوقف متجرك عن الاستعلام الدوري ويبدأ بالتفاعل. النداء الراجع موقَّع، فتستطيع إثبات أن الحمولة صادرة منّا قبل أن تتصرف بناءً عليها.
الأحداث
| الحدث | يُطلق عندما |
|---|---|
| order.completed | تم تسليم وحدة من الطلب. راجع data.status لمعرفة حالة الطلب بالكامل. |
| order.refunded | استُرد مبلغ وحدة من الطلب إلى محفظتك. تتحول قيمة data.status إلى refunded بعد استرداد جميع الوحدات. |
| webhook.test | أرسلتَ من لوحة التحكم إشعاراً تجريبياً موقّعاً لاختبار معالج الإشعارات لديك. |
رؤوس التسليم
| الرأس | القيمة |
|---|---|
| Content-Type | application/json |
| X-Shop2Topup-Signature | sha256=<hex>: قيمة HMAC-SHA256 لجسم الطلب الخام |
| X-Shop2Topup-Event | يحمل اسم الحدث الذي وقع (order.completed مثلاً) |
| User-Agent | shop2topup-webhook/1.0 |
حمولة النداء الراجع
لكل نداء راجع الحقول العليا الثلاثة نفسها — event و timestamp و data — ويعكس data كائن الطلب الذي تعيده نقطة الحالة. أكواد البطاقات موجودة فقط في الطلب المكتمل، وقد يكون player_name فارغًا، ويظهر sub_transaction_summary كلما قُسّم الطلب إلى وحدات.
{
"event": "order.completed",
"timestamp": "2026-03-11T14:30:00.000Z",
"data": {
"order_id": "01912345-6789-7abc-8def-0123456789ab",
"status": "completed",
"player_id": "123456789",
"player_name": "ProGamer99",
"subcategory_name": "FF 100 Diamonds",
"quantity": 1,
"charged_amount": "0.950000",
"currency": "USD",
"created_at": "2026-03-11T14:28:00.000Z",
"completed_at": "2026-03-11T14:30:00.000Z",
"vouchers": [
{
"code": "XXXX-YYYY-ZZZZ",
"serial_number": "SN-9928371",
"expiry_date": "2027-03-11"
}
],
"sub_transaction_summary": {
"total": 1,
"completed": 1,
"failed": 0,
"pending": 0,
"processing": 0,
"retrying": 0,
"refunded": 0
}
}
}التحقق من التوقيع
يحمل كل تسليم الرأس X-Shop2Topup-Signature، مكوَّنًا من sha256= يليه HMAC-SHA256 بصيغة hex لجسم الطلب الخام باستخدام سرّ خطاف الويب لديك. تحقّق مقابل البايتات الخام، قبل أي تحليل JSON، وقارن في زمن ثابت. المعالج الذي يحلّل أولًا ثم يتحقق يمكن خداعه بجسم أُعيدت صياغته.
// Express — verify the signature against the RAW body, not the parsed object.
const crypto = require('crypto');
app.post(
'/shop2topup/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.get('X-Shop2Topup-Signature') || '';
const expected =
'sha256=' +
crypto
.createHmac('sha256', process.env.S2T_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end();
}
const payload = JSON.parse(req.body.toString('utf8'));
// Answer fast, then do the work. The delivery times out after 10 seconds.
res.status(200).end();
enqueueSettlement(payload.data.order_id, payload.event);
},
);التسليم وإعادة المحاولة
تُجرَّب كل مناداة مرة واحدة، ولنقطتك عشر ثوانٍ للردّ. لا توجد إعادة محاولة تلقائية ولا تراجع أسّي خلفها، وهذا مقصود: نقطة الحالة هي السجل المرجعي، فالتسوية تخصّ استعلامًا تتحكم فيه أنت لا جدول إعادة محاولة لا تراه. أعد 200 فورًا ونفّذ العمل على طابورك الخاص.
معالجات idempotent
اربط معالجك بـ order_id واجعل إعادة تشغيل النداء الراجع بلا أثر. حتى مع محاولة تسليم واحدة، ستسلّم بنيتك التحتية الرسالة نفسها أحيانًا إلى عاملَين، والطلب الذي يمنح العميل رصيدًا مرتين خطأ أسوأ بكثير من طلب يصل متأخرًا.
التسجيل والأسرار
تسجيل عنوان النداء الراجع، وإرسال تسليم تجريبي موقَّع، وتدوير سرّ التوقيع — كلها تتمّ داخل لوحة الموزّع بعد تسجيل الدخول، لا عبر الواجهة البرمجية العامة. HTTPS مطلوب، والعناوين الخاصة مرفوضة.
افتح API access في لوحة الموزّعحدود المعدل ومعالجة الأخطاء
الحدود معلنة، ومحسوبة لكل حساب، ومطبَّقة بنافذة متحرّكة. يحصل عمل الكتالوج كثيف القراءة على أوسع مساحة؛ أما نقاط الحالة فمشدودة عمدًا، لأنها موجودة كمسار تسوية احتياطي لا كحلقة استعلام دوري.
| المسار | الطلبات | النافذة | تُحسب لكل |
|---|---|---|---|
| GET /catalog/* (المسارات الخمسة كلها) | 5000 | 60 ثانية | لكل حساب |
| POST /orders/create | 5000 | 60 ثانية | لكل حساب |
| GET /orders/:orderId | 3 | 60 ثانية | لكل حساب + معرّف الطلب |
| POST /orders/batch | 2 | 60 ثانية | لكل حساب |
| GET /orders | 2 | 60 ثانية | لكل حساب |
| GET /account | 60 | 60 ثانية | لكل حساب |
| POST /player/validate | الحصة اليومية للتحقق من اللاعبين | 24 ساعة | لكل حساب (بدون ترويسات X-RateLimit) |
تحمل نقاط نهاية الكتالوج والطلبات والحساب الرؤوس X-RateLimit-Limit و X-RateLimit-Remaining و X-RateLimit-Reset في كل استجابة، فلا يضطر عميل منضبط إلى التخمين. راقب عدّاد المتبقّي وخفّف وتيرتك قبل أن يُكبح طلبك لا بعده. التحقق من اللاعب هو الاستثناء: فهو يعمل ضمن حصتك اليومية لعمليات التحقق، ولا يرسل رؤوس X-RateLimit، ويذكر تلك الحصة داخل جسم استجابة 429.
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1765000000
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again after retry_after seconds.",
"action": "RETRY_LATER",
"retryable": true,
"retry_after": 45,
"details": {
"limit": 2,
"remaining": 0,
"retry_after": 45,
"window_seconds": 60
}
}
}الأخطاء التي ستقابلها أولًا، وطرق حلّها
تعيد الإخفاقات دائمًا كودًا ثابتًا إلى جانب حالة HTTP. هذه هي الأخطاء التي يصادفها أي تكامل جديد في أسبوعه الأول؛ كل واحد منها مقترن بالتغيير الذي يحلّه.
| الكود | HTTP | ماذا يعني | كيف تحلّه |
|---|---|---|---|
| INVALID_API_KEY | 401 | مفتاح API غير صالحمفتاح API المرسل لا يطابق أي مفتاح نشط في النظام. | تأكد من صحة معرّف المفتاح والسر. وعند الحاجة، أعد توليد المفتاح من لوحة الوصول إلى API.action: FIX_INPUT · retryable: false · retry_after: null |
| IP_NOT_ALLOWED | 403 | عنوان IP غير موجود في قائمة السماحصدر الاستدعاء من عنوان IP غير مدرج في قائمة السماح الخاصة بمفتاح API. | أضف عنوان IP لخادمك إلى قائمة السماح من لوحة الوصول إلى API، أو ألغِ قيود IP.action: FIX_INPUT · retryable: false · retry_after: null |
| MISSING_REQUIRED_FIELD | 400 | حقل مطلوب مفقودأحد الحقول المطلوبة غير موجود في جسم الطلب. في متطلبات اللاعب، يحدد details.field اسم الحقل المفقود. | راجع توثيق نقطة الوصول، وراجع GET /catalog/category/:categoryId/requirements لمعرفة حقول اللاعب، ثم أرسل كل الحقول المطلوبة.action: FIX_INPUT · retryable: false · retry_after: null |
| INVALID_UUID_FORMAT | 400 | صيغة UUID غير صالحةقيمة order_id ليست بصيغة UUID صالحة. | استخدم أي إصدار صالح من معرّف UUID، مثل v1 أو v4 أو v7 أو غيرها من الإصدارات. ويجب أن يأتي المعرّف على الشكل التالي: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.action: FIX_INPUT · retryable: false · retry_after: null |
| PLAYER_NOT_FOUND | 400 | اللاعب غير موجودأكدت اللعبة أن معرّف اللاعب هذا غير موجود. لا يُرسَل هذا الرمز إلا بعد تأكيد صريح، أما إذا تعذّر التحقق من المعرّف فيصلك PLAYER_CHECK_UNAVAILABLE. | تأكد من صحة معرّف اللاعب، وتحقق مما إذا كانت اللعبة تتطلب zone_id.action: FIX_INPUT · retryable: false · retry_after: null |
| PLAYER_CHECK_UNAVAILABLE | 503 | يتعذّر التحقق من معرّفات اللاعبين لهذه اللعبة حالياًلا يمكن فحص معرّفات اللاعبين في هذه اللعبة في الوقت الحالي. معرّف اللاعب لم يُرفض. | كرّر الطلب نفسه بعد retry_after ثانية. لا تخبر عميلك بأن المعرّف خاطئ.action: RETRY_LATER · retryable: true · retry_after: 60 |
| REGION_MISMATCH | 400 | لا يمكن لمنطقة هذا اللاعب استلام هذا المنتجينتمي اللاعب إلى منطقة لا يمكن توصيل هذا المنتج إليها. يتضمن الخطأ الحقلين player_region و player_name. | اختر المنتج المخصص لمنطقة اللاعب.action: CHOOSE_OTHER · retryable: false · retry_after: null |
| INSUFFICIENT_BALANCE | 400 | رصيد المحفظة غير كافٍلا يكفي رصيد محفظتك لإتمام هذا الطلب. | اشحن محفظتك من صفحة الشحن قبل تقديم الطلب.action: TOP_UP · retryable: false · retry_after: null |
| OUT_OF_STOCK | 400 | نفد مخزون المنتجالمنتج المطلوب غير متوفر في المخزون حالياً. | حاول لاحقاً أو اختر منتجاً آخر.action: RETRY_LATER · retryable: true · retry_after: null |
| PRODUCT_UNAVAILABLE | 409 | هذا المنتج غير متاح مؤقتاًالمنتج موجود لكن لا يمكن بيعه حالياً. | أعد المحاولة لاحقاً، أو اختر منتجاً مختلفاً.action: RETRY_LATER · retryable: true · retry_after: null |
| DUPLICATE_ORDER | 409 | معرّف الطلب مكرريوجد طلب سابق بهذا المعرّف UUID. هذه حماية تمنع تنفيذ الطلب نفسه مرتين. | أنشئ UUID جديداً لكل طلب جديد. إذا ظهر هذا الخطأ عند إعادة المحاولة فالمحاولة الأولى قد سُجّلت: اطّلع على ذلك الطلب عبر GET /orders/{orderId} بدلاً من إنشاء طلب آخر.action: CHECK_ORDER · retryable: false · retry_after: null |
| PRICE_INCREASED | 400 | ارتفع السعر عن المتوقعسعر الوحدة الحالي أعلى من قيمة expected_unit_price التي أرسلتها. | احصل على السعر الحالي للمنتج من المسار GET /catalog/subcategory/:id/price، ثم حدّث سعر الوحدة المتوقع في الحقل expected_unit_price داخل طلب الشراء ليساوي هذا السعر.action: FIX_INPUT · retryable: false · retry_after: null |
| RATE_LIMIT_EXCEEDED | 429 | تم تجاوز حد الطلباتلقد تجاوزت عدد الاستدعاءات المسموح به لهذا المسار. مجموعات الحدود التي تُحتسب بالدقيقة، وهي مسارات الكتالوج والطلبات والحساب، ترسل الترويسات X-RateLimit-Limit وX-RateLimit-Remaining وX-RateLimit-Reset مع كل استجابة تصلك، ويكرر الحقل details القيم limit وremaining (وتكون قيمتها 0) وretry_after وwindow_seconds. أما المسار POST /player/validate فيُحتسب استهلاكه من حصتك اليومية من عمليات التحقق من اللاعبين، ولذلك لا يرسل هذه الترويسات، ويحمل رد الخطأ 429 الخاص به القيم limit وcurrent وremaining وreset_at وreset_in_seconds داخل الكائن error. وفي كلتا الحالتين يحدد الحقل error.retry_after والترويسة Retry-After عدد الثواني التي عليك انتظارها. | انتظر عدد الثواني المذكور في retry_after أو في ترويسة Retry-After قبل إعادة المحاولة. وفي نقاط النهاية ذات الحصة بالدقيقة، راقب X-RateLimit-Remaining حتى لا تبلغ الحد أصلاً.action: RETRY_LATER · retryable: true · retry_after: ثانية حتى إعادة التعيين |
| INTERNAL_ERROR | 500 | خطأ داخلي في الخادمحدث خطأ غير متوقع في الخادم. | أعد إرسال الطلب بعد مرور retry_after ثانية. ومع POST /orders/create استخدم قيمة order_id نفسها، فلا يمكن عندها أن يُنشأ الطلب مرتين. إذا استمر الخطأ فتواصل مع الدعم.action: RETRY_LATER · retryable: true · retry_after: 30 |
أسئلة التكامل، بإجابات مباشرة
الأسئلة التي يطرحها المطوّرون فعلًا أثناء التوصيل — المصادقة، وإعادة المحاولة، و idempotency، والكبح، والنداءات الراجعة، ومعالجة الإخفاقات.
كيف تصادق واجهة الموزّعين البرمجية على الطلبات؟
برأس HTTP واحد: Authorization: Bearer <keyId>.<secret>. يُصدَر زوج المفاتيح من صفحة API Access في لوحة الموزّع، ويُعرض السرّ مرة واحدة عند الإنشاء، ويتحقق منه الخادم بفحص HMAC في كل نداء. لا يوجد تحويل OAuth، ولا كوكي جلسة، ولا رمز يحتاج إلى تجديد، فالرأس نفسه يعمل للطلب الأول ولكل طلب بعده.
ما عنوان الأساس وإصدار الواجهة الذي يقصده تطبيقي؟
عنوان أساس واحد، وهو عنوان الإنتاج — لا يوجد مضيف اختبار منفصل تبني عليه ثم تستبدله لاحقًا. وجّه تطبيقك إلى عنوان الأساس v1 الظاهر أعلاه، وأرسل Authorization: Bearer <keyId>.<secret> مع كل طلب، وابدأ بـ GET /account للتأكد من صحة المفتاح وشكل الترويسة قبل أي شيء آخر. أما POST /orders/create فهو النداء الوحيد الذي يحرّك المال، فأجّله إلى النهاية ونفّذه على أصغر فئة تبيعها مع إرسال expected_unit_price ما دام التكامل قيد الضبط.
أي حقل يحمل مفتاح idempotency في POST /orders/create؟
الحقل order_id — وهو UUID تولّده عندك وترسله في جسم الإنشاء. إن أعدت إرسال إنشاء بـ order_id موجود مسبقًا ردّت الواجهة بـ HTTP 409 والرمز DUPLICATE_ORDER بدل فتح طلب ثانٍ، فلا تتحوّل إعادة المحاولة إلى خصم ثانٍ؛ واقرأ الطلب الأصلي عبر GET /orders/:orderId. ولّد الـ UUID واحفظه قبل المحاولة الأولى، لأن UUID يُولَّد بعد انتهاء المهلة هو مفتاح جديد لا مفتاح إعادة محاولة، ولن يدمج الخادم قيمتَي order_id قصدتَ بهما الشيء نفسه.
انتهت مهلة طلبي. هل أُنشئ الطلب؟
اعرف الجواب بقراءة الحالة بدل التخمين. نادِ GET /orders/:orderId بالـ UUID نفسه الذي أرسلته: فإن عاد الطلب فمعنى ذلك أن المهلة انتهت بعد قبوله، ولا يجب أن تعيد الإرسال. وإن حصلت على ORDER_NOT_FOUND فلم يُخصم شيء، ومن الآمن تكرار طلب الإنشاء بالـ UUID نفسه.
كيف أتحقق من توقيع نداء الويب هوك؟
أعد حساب HMAC على جسم الطلب الخام وقارنه بترويسة X-Shop2Topup-Signature. تحمل الترويسة القيمة sha256=<hex>: احسب HMAC-SHA256 للبايتات كما وصلتك بالضبط باستخدام سرّ الويب هوك، وأضف البادئة sha256=، وقارن بمقارنة ثابتة الزمن — ولا تقارن أبدًا مقابل كائن JSON أُعيدت صياغته، لأن التحليل ثم إعادة التسلسل يغيّران البايتات فلا تتطابق المقارنة إطلاقًا. ارفض عدم التطابق بـ 401، وردّ على التسليم الصحيح بـ 200 خلال عشر ثوانٍ، ثم نفّذ أعمال التسوية على طابورك الخاص.
ماذا يحدث عندما أتجاوز حدّ المعدل؟
تحصل على HTTP 429 مع الكود RATE_LIMIT_EXCEEDED ورأس Retry-After. يكرّر جسم الاستجابة الرقم نفسه في retry_after ويضيف limit و remaining و window_seconds. انتظر ذلك العدد من الثواني بالضبط ثم أعد المحاولة مرة واحدة؛ لا تعد فورًا ولا توزّع النداء نفسه على عدة مفاتيح، لأن العدّاد محسوب على الحساب لا على الاتصال.
ما الحقول التي يعيدها POST /player/validate؟
يعيد الحقل player_name، ومعه zone_id و region حين توفّرهما اللعبة، داخل كائن data في الاستجابة. إنشاء الطلب نفسه يحلّ اسم اللاعب ويتحقق من المنطقة، والمحاولة من منطقة غير مسموحة تُرفض بالرمز REGION_MISMATCH وحالة HTTP 400 قبل أي خصم من المحفظة، فليس هذا النداء ما يحمي رصيدك. استدعه لأن قراءة player_name هي الطريقة العملية الوحيدة لعرض الحساب الذي يوشك المشتري على شحنه ولالتقاط معرّف مكتوب خطأً قبل أن يؤكّد — والاسم يأتي دائمًا من اللعبة، لا مما يكتبه المشتري.
كيف أحوّل استجابة requirements إلى حمولة الطلب؟
افهرسها بـ field_name: كل عنصر يعيده GET /catalog/category/:categoryId/requirements يصبح خاصية واحدة داخل كائن requirements الذي ترسله إلى POST /orders/create. ويحمل كل عنصر أيضًا data_type ليعرف نموذجك ما الذي يعرضه ويعرف خادمك كيف يحوّل القيمة، أما عناصر single_select و multi_select فتأتي بقيمها المسموحة — اعرضها بدل حقل نصّي حر. خزّن القائمة لكل فئة بدل قراءتها مع كل طلب، وأعد قراءتها إذا رجع نداء الإنشاء بالرمز MISSING_REQUIRED_FIELD.
كيف يتعامل تطبيقي مع استجابة PRICE_INCREASED؟
أعد قراءة السعر ثم قرّر ثم أعد الإرسال — ولا تُعِد إرسال الجسم نفسه أبدًا. يعني الرمز PRICE_INCREASED وحالة HTTP 400 أن سعر الوحدة الحالي أعلى من expected_unit_price الذي أرسلته، فرُفض الإنشاء ولم يُخصم شيء؛ وتحمل تفاصيل الخطأ كلًّا من expected_unit_price و current_unit_price، وإعادة المحاولة بالرقم القديم ستُرفض في كل مرة. اقرأ GET /catalog/subcategory/:itemId/price، وقرّر إن كان الرقم الجديد يناسبك، ثم أعد الإرسال بـ expected_unit_price المحدَّث وبنفس order_id — فالمحاولة المرفوضة لم تستهلكه. وحذف expected_unit_price يعطّل الحماية تمامًا، وهو ما لا يريده تكامل آلي غالبًا.
كيف يظهر الطلب المسلَّم جزئيًا في استجابة الطلب؟
تكون الحالة partial، والتفصيل داخل sub_transaction_summary. يعدّ هذا الكائن الوحدات بوصفها completed أو refunded أو pending — ومجموع هذه الثلاثة يساوي total دائمًا — بينما يسرد sub_transactions كل وحدة بحالتها الخاصة. تبقى الوحدة التي لم تُسلَّم ولم تُسترد بعد في حالة pending. عامل pending كحالة غير نهائية وواصل الانتظار، وعامل completed و partial و refunded كحالات نهائية. أما العدّادات failed و processing و retrying فتبقى للتوافق فقط وقيمتها دائمًا 0. وتحمل نداءات order.completed و order.refunded الملخص نفسه، فتستطيع تسوية حمولة موثّقة التوقيع دون قراءة ثانية.
بماذا يُردّ على طلب قادم من عنوان خارج قائمة السماح؟
بحالة HTTP 403 والرمز IP_NOT_ALLOWED، ويُردّ قبل تشغيل أي منطق عمل. يسري الفحص كلما كانت قائمة عناوين IP المسموحة للمفتاح غير فارغة، فيبقى المفتاح المسرَّب بلا فائدة خارج عناوين الخروج الخاصة بك. تفرّع على error.code لا على نص الرسالة، واعتبر ظهور IP_NOT_ALLOWED فجأة في الإنتاج تغيّرًا في عنوان الخروج — بوابة NAT جديدة أو شبكة عمّال جديدة — لا مفتاحًا معطوبًا. ولا تترك القائمة فارغة إلا إذا كان عنوان خروجك غير ثابت فعلًا.
كم يستغرق الطلب حتى يكتمل؟
تعامل مع الاكتمال على أنه غير متزامن، لا فوري أبدًا. تعيد POST /orders/create فور قبول الطلب وخصم المحفظة، عادةً بالحالة pending؛ ثم يجري التنفيذ على عمّال منفصلين وتصلك الحالة النهائية عبر webhook أو عند قراءتك التالية للحالة. لا تحجز طلب HTTP موجَّهًا للعميل بانتظار الحالة النهائية — اقبله، وأجب عميلك، وسوِّ الأمر خارج المسار.
أنشئ حسابًا، وولّد مفتاحًا، وانطلق
لا يوجد طابور موافقات ولا فترة انتظار بين التسجيل ومناداة الواجهة البرمجية. أنشئ حساب الموزّع، وولّد مفتاحًا من صفحة API Access، وموّل المحفظة، ليخرج أول طلب لك في اليوم نفسه.
لست المطوّر في هذا المشروع؟ الجانب التجاري مشروح في صفحة برنامج موزّعي SHOP2TOPUP.