REST API — v1
API شارژ بازی و ووچر برای نمایندگان فروش
یک REST API برای شارژ بازی، شارژ حساب بازی و کدهای ووچر: کاتالوگ را بخوانید، بازیکن را اعتبارسنجی کنید، سفارش ثبت کنید و نتیجه را با وبهوک یا خواندن وضعیت بگیرید.
این همان API شارژ host-to-host یا H2H است که پشت SHOP2TOPUP کار میکند. یک JSON API ساده روی HTTPS با احراز هویت Bearer، کلیدهای idempotency مبتنی بر UUID، محدودیتهای نرخ اعلامشده و کدهای خطای پایدار و ماشینخوان؛ بنابراین یک فروشگاه، یک ویترین یا یک بات میتواند بدون هیچ کتابخانه کلاینتی، اعتبار دیجیتال بازی را بهصورت برنامهنویسیشده بفروشد. همین رابط ۳,۰۰۰,۰۰۰+ سفارش تحویلشده را در ۵+ سال کار پیوسته پشت سر گذاشته است.
به دنبال قیمتها، روشهای پرداخت و نحوه کار خود حساب شریک هستید؟ این را بخوانید: معرفی برنامه نمایندگی 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"
}
}
}کل سطح API در یک صفحه
یازده اندپوینت تمام یکپارچهسازی را پوشش میدهند: خواندن کاتالوگ، قیمتگذاری یک آیتم، بررسی بازیکن، ثبت سفارش و مغایرتگیری پس از آن. هر ردیف مستقیماً به مرجع کامل، با تمام پارامترها، یک نمونه درخواست و یک نمونه پاسخ، پیوند میخورد.
| متد | مسیر | چه کاری میکند | مرجع |
|---|---|---|---|
| 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 شامل شناسه کلید و 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
کاتالوگ را بخوانید
از دستههای بزرگ به دستهها و از آنجا به زیردستهها بروید، سپس قیمت فعلی آیتمی را که میخواهید بفروشید بخوانید. درخت را کش کنید، قیمت را تازه بگیرید.
- 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 میتواند null باشد و 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= و پس از آن مقدار hex از HMAC-SHA256 روی بدنه خام درخواست با کلید وبهوک شماست. پیش از هر پارس 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 بسازید و تکرار یک کالبک را بیاثر کنید. حتی با یک بار تلاش تحویل، زیرساخت خودتان گاهی همان پیام را به دو ورکر میدهد و سفارشی که دو بار به مشتری اعتبار بدهد باگ بسیار بدتری از سفارشی است که دیر اعتبار میدهد.
ثبت و کلیدهای محرمانه
ثبت آدرس کالبک، ارسال یک تحویل آزمایشی امضاشده و تعویض کلید امضا، همگی داخل پنل نمایندگی و پس از ورود به حساب انجام میشوند، نه از طریق API عمومی. HTTPS الزامی است و آدرسهای خصوصی پذیرفته نمیشوند.
باز کردن API access در پنل نمایندگیمحدودیتهای نرخ و مدیریت خطا
محدودیتها اعلامشدهاند، بهازای هر حساب شمرده میشوند و با پنجره لغزان اعمال میشوند. بیشترین سهم به کارهای خواندنی کاتالوگ میرسد؛ اندپوینتهای وضعیت عمداً تنگ هستند، چون بهعنوان مسیر پشتیبان مغایرتگیری وجود دارند نه حلقه بررسی دائمی.
| مسیر | درخواست | پنجره | شمارش بر اساس |
|---|---|---|---|
| GET /catalog/* (هر پنج مسیر) | 5000 | ۶۰ ثانیه | برای هر حساب |
| POST /orders/create | 5000 | ۶۰ ثانیه | برای هر حساب |
| GET /orders/:orderId | 3 | ۶۰ ثانیه | برای هر حساب و شناسه سفارش |
| POST /orders/batch | 2 | ۶۰ ثانیه | برای هر حساب |
| GET /orders | 2 | ۶۰ ثانیه | برای هر حساب |
| GET /account | 60 | ۶۰ ثانیه | برای هر حساب |
| POST /player/validate | سهمیه روزانه بررسی بازیکن | ۲۴ ساعت | برای هر حساب (بدون هدرهای X-RateLimit) |
اندپوینتهای کاتالوگ، سفارش و حساب در هر پاسخ هدرهای X-RateLimit-Limit، X-RateLimit-Remaining و X-RateLimit-Reset را حمل میکنند، بنابراین یک کلاینت درسترفتار هرگز مجبور نیست حدس بزند کجا ایستاده است. شمارنده remaining را دنبال کنید و پیش از محدود شدن سرعت را کم کنید، نه پس از آن. تأیید بازیکن استثناست: با سهمیه روزانه بررسی بازیکن شما کار میکند، هدر 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 ارسالشده با هیچ کلید فعالی در سیستم مطابقت ندارد. | درستی شناسه کلید و secret خود را بررسی کنید. در صورت نیاز، کلید را از پنل API Access دوباره بسازید.action: FIX_INPUT · retryable: false · retry_after: null |
| IP_NOT_ALLOWED | 403 | آدرس IP در فهرست مجاز نیستدرخواست از آدرس IPای ارسال شده که در فهرست مجاز کلید API شما نیست. | آدرس IP سرور خود را در پنل API Access به فهرست مجاز اضافه کنید یا محدودیت 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 | موجودی کیف پول کافی نیستکیف پول شما برای تکمیل این سفارش موجودی کافی ندارد. | پیش از ثبت سفارش، کیف پول خود را از صفحه Reload شارژ کنید.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 از قبل وجود دارد. این خطا برای جلوگیری از ثبت تکراری (idempotency) است. | برای سفارش جدید یک 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 (برابر ۰)، retry_after و window_seconds را تکرار میکند. اما POST /player/validate بهجای این محدودیتها با سهمیه روزانه بررسی بازیکن شما سنجیده میشود: این مسیر هیچیک از هدرهای X-RateLimit را نمیفرستد و پاسخ 429 آن مقادیر limit، current، remaining، reset_at و reset_in_seconds را داخل شیء error برمیگرداند. در هر دو حالت، مقدار error.retry_after و هدر Retry-After تعداد ثانیههایی را نشان میدهند که باید پیش از تلاش دوباره صبر کنید. | پیش از تلاش دوباره، به اندازه ثانیههای retry_after یا هدر Retry-After صبر کنید. در endpointهای دقیقهای، 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، محدودیت نرخ، کالبکها و مدیریت خطا.
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 یا pending میشمارد — جمع این سه همیشه برابر total است — و sub_transactions هر واحد را با وضعیت خودش فهرست میکند. واحدی که هنوز نه تحویل شده و نه بازپرداخت، pending میماند. pending را غیرنهایی بدانید و منتظر بمانید؛ completed، partial و refunded نهاییاند. شمارندههای failed، processing و retrying فقط برای سازگاری حفظ شدهاند و همیشه ۰ هستند. فراخوانهای order.completed و 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.