REST API - v1

واجهة برمجية لشحن الألعاب والبطاقات للموزّعين

واجهة REST واحدة لشحن الألعاب وأكواد البطاقات: اقرأ الكتالوج، وتحقّق من اللاعب، وأنشئ الطلب، واستقبل النتيجة عبر webhook أو باستعلام الحالة.

هذه هي واجهة الشحن المباشر بين الخوادم (H2H) التي تقف خلف SHOP2TOPUP. إنها واجهة JSON عادية فوق HTTPS مع مصادقة Bearer، ومفاتيح idempotency بصيغة UUID، وحدود معدل معلنة، وأكواد أخطاء ثابتة يقرأها الحاسوب، فيستطيع أي متجر أو واجهة بيع أو بوت أن يبيع رصيد الألعاب الرقمي برمجيًا دون مكتبة عميل. وقد حملت الواجهة نفسها أكثر من 3,000,000 طلب مُسلَّم على مدى أكثر من 5 سنوات من التشغيل المتواصل.

تبحث عن الأسعار ووسائل الدفع وكيفية عمل حساب الشريك نفسه؟ اقرأ نظرة عامة على برنامج موزّعي SHOP2TOPUP.

نداؤك الأول، قبل أن توقّع أي شيء

كل نقطة نهاية هي مجرد طلب HTTPS عادي يحمل رأسًا واحدًا. لا SDK لتثبيته، ولا مكتبة عميل لإدراجها، ولا خطوة بناء لإضافتها. الصق النداء أدناه، وضع مفتاحك، لتحصل على استجابة كتالوج حيّة في طرفيّتك.

الطلب - cURL
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
  -H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"
الاستجابة - 200 OK
{
  "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",
    "details": {
      "expected_unit_price": "0.950000",
      "current_unit_price": "0.980000"
    }
  }
}

سطح الواجهة البرمجية كاملًا في شاشة واحدة

إحدى عشرة نقطة نهاية تغطي التكامل بأكمله: اقرأ الكتالوج، وسعّر عنصرًا، وتحقّق من لاعب، وأنشئ طلبًا، ثم سوِّه محاسبيًا بعد ذلك. كل صفّ يقود مباشرةً إلى المرجع الكامل، بكل معاملاته ومثال طلب ومثال استجابة.

الطريقةالمسارماذا تفعلالمرجع
GET/accountGet Account Infoاقرأ المرجع
GET/catalog/big-categoriesList Big Categoriesاقرأ المرجع
GET/catalog/categoriesList Categoriesاقرأ المرجع
GET/catalog/subcategoriesList Subcategories (Products)اقرأ المرجع
GET/catalog/subcategory/:itemId/priceGet Item Priceاقرأ المرجع
GET/catalog/category/:categoryId/requirementsGet Category Requirementsاقرأ المرجع
POST/player/validateValidate Playerاقرأ المرجع
POST/orders/createCreate Orderاقرأ المرجع
GET/orders/:orderIdGet Order Statusاقرأ المرجع
POST/orders/batchBatch Get Ordersاقرأ المرجع
GET/ordersList 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. 1

    اقرأ الكتالوج

    انتقل من الفئات الكبرى إلى الفئات ثم إلى الفئات الفرعية، ثم اقرأ السعر الحالي للعنصر الذي توشك أن تبيعه. خزّن الشجرة مؤقتًا؛ وسعّر العنصر لحظيًا.

  2. 2

    تحقّق من اللاعب

    حلّ المعرّف إلى اسم داخل اللعبة وإلى منطقة قبل تحرّك أي مال. هنا تُلتقط الأخطاء المطبعية وأخطاء المناطق.

  3. 3

    أنشئ الطلب

    ولّد UUID، واحفظه، ثم أرسله بوصفه order_id. هو مفتاح idempotency الخاص بك، وهو الطريقة الآمنة الوحيدة لإعادة محاولة طلب إنشاء.

  4. 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.completedThe order finished and everything it owed the buyer was delivered.
order.failedThe order failed and no sub-transaction is left pending or processing.
order.refundedThe order was fully refunded back to your wallet.
webhook.testYou triggered a signed test delivery from the panel to check your handler.

رؤوس التسليم

الرأسالقيمة
Content-Typeapplication/json
X-Shop2Topup-Signaturesha256=<hex> — HMAC-SHA256 of the raw request body
X-Shop2Topup-EventEvent name, e.g. order.completed
User-Agentshop2topup-webhook/1.0

حمولة النداء الراجع

لكل نداء راجع الحقول العليا الثلاثة نفسها — event و timestamp و data — ويعكس data كائن الطلب الذي تعيده نقطة الحالة. أكواد البطاقات موجودة فقط في الطلب المكتمل، وقد يكون player_name فارغًا، ويظهر sub_transaction_summary كلما قُسّم الطلب إلى وحدات.

جسم POST - order.completed
{
  "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، وقارن في زمن ثابت. المعالج الذي يحلّل أولًا ثم يتحقق يمكن خداعه بجسم أُعيدت صياغته.

Node.js - التحقق والإقرار
// 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/* (all five)8060sper account
POST /orders/create12060sper account
GET /orders/:orderId360sper account + order id
POST /orders/batch260sper account
GET /orders260sper account
GET /account6060sper account
POST /player/validatededicated limiterper account

تحمل كل استجابة الرؤوس X-RateLimit-Limit و X-RateLimit-Remaining و X-RateLimit-Reset، فلا يضطر عميل منضبط إلى التخمين. راقب عدّاد المتبقّي وخفّف وتيرتك قبل أن يُكبح طلبك لا بعده.

استجابة الكبح - 429
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1765000000

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Try again in 45 seconds.",
    "details": {
      "limit": 80,
      "remaining": 0,
      "retry_after": 45,
      "window_seconds": 60
    }
  }
}

الأخطاء التي ستقابلها أولًا، وطرق حلّها

تعيد الإخفاقات دائمًا كودًا ثابتًا إلى جانب حالة HTTP. هذه هي الأخطاء التي يصادفها أي تكامل جديد في أسبوعه الأول؛ كل واحد منها مقترن بالتغيير الذي يحلّه.

الكودHTTPماذا يعنيكيف تحلّه
INVALID_API_KEY401Invalid API keyThe provided API key does not match any active key in the system.Verify your key ID and secret are correct. Regenerate the key from the API Access panel if needed.
IP_NOT_ALLOWED403IP address not in allowlistThe request originated from an IP address not in your API key allowlist.Add your server IP to the allowlist in the API Access panel, or remove IP restrictions.
MISSING_REQUIRED_FIELD400Missing required fieldA required field is missing from the request body.Check the endpoint documentation for required fields.
INVALID_UUID_FORMAT400Invalid UUID formatThe order_id is not a valid UUID format.Use any valid UUID format (v1, v4, v7, etc). Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
PLAYER_NOT_FOUND400Player not foundThe player ID could not be found in the game system.Verify the player ID is correct. Check if a zone_id is required for the game.
REGION_MISMATCH400Region mismatchThe player belongs to a different region than expected.Use the correct zone_id or genshin_zone for the player region.
INSUFFICIENT_BALANCE400Insufficient wallet balanceYour wallet does not have enough funds to complete this order.Top up your wallet from the Reload page before placing the order.
OUT_OF_STOCK400Product is out of stockThe requested product is currently unavailable.Try again later or choose a different product.
DUPLICATE_ORDER409Duplicate order IDAn order with this UUID already exists. This is an idempotency protection.Generate a new UUID for a new order. If retrying, use the same UUID to get the existing order.
PRICE_INCREASED400Price has increased beyond expectedThe current unit price is higher than the expected_unit_price you provided.Fetch the latest price with GET /catalog/subcategory/:id/price and update expected_unit_price.
RATE_LIMIT_EXCEEDED429Rate limit exceededYou have exceeded the rate limit for this endpoint. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. When exceeded, the 429 response body contains: limit, remaining (0), retry_after (seconds to wait), and window_seconds. A Retry-After header is also set.Wait for the number of seconds indicated by retry_after or the Retry-After header before retrying. Monitor X-RateLimit-Remaining headers to avoid hitting limits.
SERVICE_UNAVAILABLE503Service temporarily unavailableThe service is temporarily down for maintenance or overloaded.Wait and retry after a few minutes.
اطّلع على مرجع أكواد الأخطاء الكامل — 32 كودًا مع طرق الحلّ

أسئلة التكامل، بإجابات مباشرة

الأسئلة التي يطرحها المطوّرون فعلًا أثناء التوصيل — المصادقة، وإعادة المحاولة، و 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 و failed و pending و processing و retrying — بينما يحمل sub_transactions السطر الذي خلف كل عدد، فيقرأ منطق التسوية لديك أرقامًا بدل استنتاجها من كلمة واحدة. عامل pending و processing كحالتين غير نهائيتين وواصل الانتظار، وعامل completed و partial و failed و refunded كحالات نهائية. وتحمل نداءات order.completed و order.failed و order.refunded الملخص نفسه، فتستطيع تسوية حمولة موثّقة التوقيع دون قراءة ثانية.

بماذا يُردّ على طلب قادم من عنوان خارج قائمة السماح؟

بحالة HTTP 403 والرمز IP_NOT_ALLOWED، ويُردّ قبل تشغيل أي منطق عمل. يسري الفحص كلما كانت قائمة عناوين IP المسموحة للمفتاح غير فارغة، فيبقى المفتاح المسرَّب بلا فائدة خارج عناوين الخروج الخاصة بك. تفرّع على error.code لا على نص الرسالة، واعتبر ظهور IP_NOT_ALLOWED فجأة في الإنتاج تغيّرًا في عنوان الخروج — بوابة NAT جديدة أو شبكة عمّال جديدة — لا مفتاحًا معطوبًا. ولا تترك القائمة فارغة إلا إذا كان عنوان خروجك غير ثابت فعلًا.

كم يستغرق الطلب حتى يكتمل؟

تعامل مع الاكتمال على أنه غير متزامن، لا فوري أبدًا. تعيد POST /orders/create فور قبول الطلب وخصم المحفظة، عادةً بالحالة pending؛ ثم يجري التنفيذ على عمّال منفصلين وتصلك الحالة النهائية عبر webhook أو عند قراءتك التالية للحالة. لا تحجز طلب HTTP موجَّهًا للعميل بانتظار الحالة النهائية — اقبله، وأجب عميلك، وسوِّ الأمر خارج المسار.

أنشئ حسابًا، وولّد مفتاحًا، وانطلق

لا يوجد طابور موافقات ولا فترة انتظار بين التسجيل ومناداة الواجهة البرمجية. أنشئ حساب الموزّع، وولّد مفتاحًا من صفحة API Access، وموّل المحفظة، ليخرج أول طلب لك في اليوم نفسه.

لست المطوّر في هذا المشروع؟ الجانب التجاري مشروح في صفحة برنامج موزّعي SHOP2TOPUP.

واجهة شحن الألعاب والبطاقات للموزّعين — وثائق REST API