REST API — v1

API شارژ بازی و ووچر برای نمایندگان فروش

یک REST API برای شارژ بازی، شارژ حساب بازی و کدهای ووچر: کاتالوگ را بخوانید، بازیکن را اعتبارسنجی کنید، سفارش ثبت کنید و نتیجه را با وب‌هوک یا خواندن وضعیت بگیرید.

این همان API شارژ host-to-host یا H2H است که پشت SHOP2TOPUP کار می‌کند. یک JSON API ساده روی 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"
    }
  }
}

کل سطح API در یک صفحه

یازده اندپوینت تمام یکپارچه‌سازی را پوشش می‌دهند: خواندن کاتالوگ، قیمت‌گذاری یک آیتم، بررسی بازیکن، ثبت سفارش و مغایرت‌گیری پس از آن. هر ردیف مستقیماً به مرجع کامل، با تمام پارامترها، یک نمونه درخواست و یک نمونه پاسخ، پیوند می‌خورد.

متدمسیرچه کاری می‌کندمرجع
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 شامل شناسه کلید و secret را در بر دارد و از صفحه API Access پنل نمایندگی شما صادر می‌شود. شناسه کلید حساب را مشخص می‌کند؛ secret مالکیت شما را ثابت می‌کند و در سمت سرور با یک بررسی HMAC تأیید می‌شود.

مقدار secret تنها یک بار، هنگام ساخت، نمایش داده می‌شود و پس از آن قابل بازیابی نیست. آن را در همان مدیر رمز پشته خودتان بگذارید، به‌صورت متغیر محیطی تزریق کنید و بیرون از کنترل نسخه نگه دارید. اگر گمش کنید، بازیابی نمی‌کنید، تعویض می‌کنید.

نه ریدایرکت OAuth هست، نه کوکی نشست، نه توکن تازه‌سازی و نه چیزی برای زمان‌بندی. این موضوع برای یکپارچه‌سازی‌های خودکار مهم است: یک کرون‌جاب یا یک ورکر صف می‌تواند سال‌ها همان اعتبار را نگه دارد بدون اینکه مسیر تمدیدی لازم باشد، و ساعت انقضایی وجود ندارد که ساعت سه بامداد شما را بیدار کند.

می‌توان یک فهرست IP مجاز به کلید متصل کرد. به‌محض اینکه این فهرست خالی نباشد، درخواست‌ها از هر آدرس دیگری پیش از اجرای هر منطق تجاری با IP_NOT_ALLOWED و HTTP 403 رد می‌شوند، بنابراین یک اعتبار لو رفته بیرون از سرورهای خودتان بی‌فایده است. تعویض کلید یک استقرار است نه یک مهاجرت: کلید جدید را بسازید، منتشر کنید، قدیمی را حذف کنید.

کلیدها به یک حساب نمایندگی تعلق دارند و از کیف پول همان حساب خرج می‌کنند، بنابراین هرگز secret را به مرورگر، به بسته موبایل یا به یک مخزن عمومی نفرستید. API را از بک‌اند خودتان صدا بزنید و فرانت‌اند خودتان با بک‌اند خودتان صحبت کند. هر چیزی که اینجا مستند شده یک فراخوان سرور به سرور را فرض می‌گیرد.

هدر درخواست
Authorization: Bearer <keyId>.<secret>

بررسی یک کلید با یک فراخوانی

GET /account همان بررسی سلامت برای یک اعتبار است. هم‌زمان به سه پرسش پاسخ می‌دهد: آیا کلید احراز هویت می‌شود، آیا حساب فعال است و آیا کیف پول برای چیزی که می‌خواهید سفارش دهید کافی است. آن را هنگام راه‌اندازی و پیش از هر اجرای دسته‌ای صدا بزنید.

خواندن مرجع احراز هویت و حساب

یک یکپارچه‌سازی کامل، از ابتدا تا انتها

یک یکپارچه‌سازی کارآمد چهار مرحله دارد. نمونه‌های زیر همه آن‌ها را روی API زنده با 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 می‌تواند null باشد و 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= و پس از آن مقدار hex از HMAC-SHA256 روی بدنه خام درخواست با کلید وب‌هوک شماست. پیش از هر پارس 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 بسازید و تکرار یک کال‌بک را بی‌اثر کنید. حتی با یک بار تلاش تحویل، زیرساخت خودتان گاهی همان پیام را به دو ورکر می‌دهد و سفارشی که دو بار به مشتری اعتبار بدهد باگ بسیار بدتری از سفارشی است که دیر اعتبار می‌دهد.

ثبت و کلیدهای محرمانه

ثبت آدرس کال‌بک، ارسال یک تحویل آزمایشی امضاشده و تعویض کلید امضا، همگی داخل پنل نمایندگی و پس از ورود به حساب انجام می‌شوند، نه از طریق API عمومی. 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 را حمل می‌کند، بنابراین یک کلاینت درست‌رفتار هرگز مجبور نیست حدس بزند کجا ایستاده است. شمارنده remaining را دنبال کنید و پیش از محدود شدن سرعت را کم کنید، نه پس از آن.

پاسخ محدودشده — 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، محدودیت نرخ، کال‌بک‌ها و مدیریت خطا.

API نمایندگی چگونه درخواست‌ها را احراز هویت می‌کند؟

با یک هدر HTTP: Authorization: Bearer <keyId>.<secret>. جفت کلید از صفحه API Access در پنل نمایندگی شما صادر می‌شود، مقدار secret تنها یک بار هنگام ساخت نمایش داده می‌شود و سرور در هر فراخوانی آن را با یک بررسی HMAC تأیید می‌کند. نه ریدایرکت OAuth وجود دارد، نه کوکی نشست و نه توکنی که باید تازه شود؛ همان یک هدر برای اولین درخواست و هر درخواست پس از آن کار می‌کند.

کلاینت من باید به کدام آدرس پایه و کدام نسخه وصل شود؟

فقط یک آدرس پایه وجود دارد و همان هم محیط واقعی است؛ هاست سندباکس جداگانه‌ای نیست که اول روی آن بسازید و بعد عوضش کنید. کلاینت را به همان آدرس پایه v1 بالا وصل کنید، در هر درخواست Authorization: Bearer <keyId>.<secret> بفرستید و پیش از هر چیز با GET /account درستی کلید و شکل هدر را ثابت کنید. تنها فراخوانی که پول جابه‌جا می‌کند POST /orders/create است، پس آن را برای آخر بگذارید، روی کوچک‌ترین مقدار اجرا کنید و تا وقتی کلاینت هنوز در حال تنظیم است expected_unit_price را همراهش بفرستید.

کلید idempotency در POST /orders/create روی کدام فیلد است؟

روی فیلد order_id — یک UUID که خودتان در سمت کلاینت می‌سازید و در بدنه ساخت می‌فرستید. اگر ساخت را با order_id تکراری دوباره بفرستید، API به‌جای باز کردن سفارش دوم با HTTP 409 و کد DUPLICATE_ORDER پاسخ می‌دهد، بنابراین تکرار درخواست هرگز به کسر دوم تبدیل نمی‌شود؛ سفارش اصلی را با GET /orders/:orderId بخوانید. این UUID را پیش از اولین تلاش بسازید و ذخیره کنید، چون UUID ساخته‌شده بعد از تایم‌اوت یک کلید تازه است نه کلید تکرار، و سرور دو order_id را که منظورتان از هر دو یک چیز بوده ادغام نمی‌کند.

درخواست من تایم‌اوت شد. آیا سفارش ساخته شد؟

به‌جای حدس زدن، وضعیت را بخوانید. با همان UUID که فرستادید GET /orders/:orderId را صدا بزنید: اگر سفارش برگشت، تایم‌اوت پس از پذیرش آن رخ داده و نباید دوباره بفرستید. اگر 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 همان خلاصه را حمل می‌کنند، پس یک payload با امضای تأییدشده بدون خواندن دوم هم قابل تسویه است.

درخواستی که از آدرس خارج از فهرست مجاز بیاید چه می‌گیرد؟

وضعیت HTTP 403 با کد IP_NOT_ALLOWED، آن هم پیش از اجرای هر منطق کسب‌وکار. این بررسی هر وقت فهرست IP مجاز کلید خالی نباشد اعمال می‌شود و کلید لو رفته را بیرون از آدرس‌های خروجی خودتان بی‌اثر می‌کند. به‌جای متن پیام روی error.code شاخه بزنید و دیدن ناگهانی IP_NOT_ALLOWED در محیط واقعی را نشانه تغییر آدرس خروجی بدانید — یک NAT gateway تازه یا ساب‌نت جدید ورکرها — نه کلید خراب. فهرست را فقط وقتی خالی بگذارید که آدرس خروجی‌تان واقعاً ثابت نباشد.

تکمیل یک سفارش چقدر طول می‌کشد؟

تکمیل را غیرهمزمان در نظر بگیرید، نه آنی. POST /orders/create به‌محض پذیرش سفارش و کسر از کیف پول پاسخ می‌دهد، معمولاً با وضعیت pending؛ سپس انجام کار روی پردازشگرهای جداگانه ادامه می‌یابد و وضعیت نهایی با وب‌هوک یا در خواندن بعدی وضعیت به شما می‌رسد. یک درخواست HTTP رو به مشتری را برای انتظار وضعیت نهایی مسدود نکنید — سفارش را بپذیرید، به مشتری پاسخ دهید و تسویه را بیرون از آن مسیر کامل کنید.

حساب بسازید، کلید بگیرید، راه بیفتید

میان ثبت‌نام و فراخوانی API نه صف تأییدی هست و نه دوره انتظاری. حساب نمایندگی را بسازید، از صفحه API Access یک کلید بسازید، کیف پول را شارژ کنید و اولین سفارش‌تان می‌تواند همان روز ثبت شود.

توسعه‌دهنده این پروژه شما نیستید؟ بخش تجاری اینجا توضیح داده شده است: صفحه برنامه نمایندگی SHOP2TOPUP.

API شارژ بازی و ووچر برای نمایندگان فروش — REST API