REST API - versi 1

API Top Up Game & Baucar untuk Reseller

Satu REST API untuk top up game, reload game dan kod baucar: baca katalog, sahkan pemain, buat pesanan, kemudian terima keputusannya melalui webhook atau semakan status.

Ini ialah API top up host-to-host (H2H) yang menggerakkan SHOP2TOPUP. Ia API JSON biasa melalui HTTPS dengan pengesahan Bearer, kunci idempotensi UUID, had kadar yang diterbitkan dan kod ralat stabil yang boleh dibaca mesin. Kedai, storefront atau bot boleh menjual kredit game digital secara programatik tanpa perlukan pustaka klien. Antara muka yang sama telah menghantar 3,000,000+ pesanan sepanjang 5+ tahun beroperasi tanpa henti.

Untuk harga, kaedah pembayaran dan cara akaun rakan kongsi itu sendiri berfungsi, baca gambaran keseluruhan program reseller SHOP2TOPUP.

Panggilan pertama, sebelum anda tandatangan apa-apa

Setiap endpoint ialah permintaan HTTPS biasa dengan satu header. Tiada SDK untuk dipasang, tiada pustaka klien untuk dimasukkan ke dalam projek dan tiada langkah build tambahan. Tampal panggilan di bawah, masukkan kunci anda, dan respons katalog sebenar terus muncul di terminal anda.

Permintaan - cURL
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
  -H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"
Respons - 200 OK
{
  "success": true,
  "categories": [
    {
      "id": 12,
      "name": "Free Fire",
      "description": "Garena Free Fire diamonds",
      "big_category_id": 1,
      "big_category_name": "Mobile Games"
    }
  ]
}

URL asas

https://shop2topup.com/api/endpoints/v1

Setiap laluan dalam dokumentasi adalah relatif kepada URL asas ini. Respons dalam format JSON dan sentiasa mengandungi medan boolean success, jadi satu cabang dalam klien anda cukup untuk memisahkan laluan berjaya daripada keadaan lain.

Struktur ralat

Ralat mengekalkan bentuk yang sama pada setiap kod status. Gunakan error.code untuk cabang logik anda kerana ia sebahagian daripada kontrak. Jangan bergantung pada error.message, kerana mesej itu ditulis untuk manusia dan susunan katanya boleh berubah bila-bila masa.

Respons ralat
{
  "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"
    }
  }
}

Seluruh API dalam satu skrin

Sebelas endpoint merangkumi keseluruhan integrasi: baca katalog, dapatkan harga item, sahkan pemain, buat pesanan dan padankan rekodnya selepas itu. Setiap baris dipautkan terus ke rujukan penuh, lengkap dengan setiap parameter, contoh permintaan dan contoh respons.

KaedahLaluanFungsiRujukan
GET/accountDapatkan maklumat akaunBaca rujukan
GET/catalog/big-categoriesSenarai kategori besarBaca rujukan
GET/catalog/categoriesSenarai kategoriBaca rujukan
GET/catalog/subcategoriesSenarai subkategori (produk)Baca rujukan
GET/catalog/subcategory/:itemId/priceDapatkan harga itemBaca rujukan
GET/catalog/category/:categoryId/requirementsDapatkan keperluan kategoriBaca rujukan
POST/player/validateSahkan pemainBaca rujukan
POST/orders/createBuat pesananBaca rujukan
GET/orders/:orderIdDapatkan status pesananBaca rujukan
POST/orders/batchDapatkan pesanan secara pukalBaca rujukan
GET/ordersSenarai pesananBaca rujukan

Juga ada webhook status pesanan, diterangkan sepenuhnya di bahagian bawah halaman ini.

Pengesahan dan kawalan akses

Pengesahan hanya memerlukan satu header HTTP. Setiap permintaan membawa header Authorization yang mengandungi kelayakan Bearer, iaitu gabungan ID kunci dan kunci rahsia, yang dikeluarkan dari halaman Akses API dalam panel reseller anda. ID kunci mengenal pasti akaun, manakala kunci rahsia membuktikan anda pemiliknya dan disahkan di server melalui semakan HMAC.

Kunci rahsia hanya dipaparkan sekali, semasa ia dicipta, dan tidak boleh diambil semula selepas itu. Simpan dalam secret manager yang sudah digunakan oleh sistem anda, suntik sebagai pemboleh ubah persekitaran dan jangan masukkan ke dalam kawalan sumber. Jika hilang, anda buat kunci baharu, bukan memulihkan yang lama.

Tiada redirect OAuth, tiada kuki sesi, tiada refresh token dan tiada apa-apa untuk dijadualkan. Ini penting untuk integrasi automatik: cron job atau queue worker boleh memegang kelayakan yang sama bertahun-tahun tanpa perlu diperbaharui, dan tiada tarikh tamat tempoh yang mengejutkan anda pada pukul tiga pagi.

Senarai IP yang dibenarkan (allowlist) boleh dipasang pada kunci. Apabila senarai itu tidak kosong, permintaan dari alamat lain ditolak dengan IP_NOT_ALLOWED dan HTTP 403 sebelum sebarang logik perniagaan dijalankan, jadi kelayakan yang bocor tidak berguna di luar server anda sendiri. Menukar kunci cuma perlukan satu deploy, bukan migrasi: buat kunci baharu, lancarkan, kemudian padam kunci lama.

Setiap kunci milik satu akaun reseller dan membelanjakan dompet akaun itu, jadi jangan sesekali letak kunci rahsia dalam pelayar, bundle aplikasi mudah alih atau repositori awam. Panggil API dari backend anda dan biarkan front end anda berhubung dengan backend itu. Semua yang didokumenkan di sini menganggap panggilan dibuat dari server ke server.

Header permintaan
Authorization: Bearer <keyId>.<secret>

Sahkan kunci dengan satu panggilan

GET /account ialah semakan kesihatan untuk kelayakan anda. Ia menjawab tiga soalan serentak: adakah kunci berjaya disahkan, adakah akaun diaktifkan, dan adakah baki dompet mencukupi untuk pesanan yang akan anda buat. Jalankan semasa sistem dimulakan dan sebelum setiap larian pukal.

Baca rujukan pengesahan dan akaun

Integrasi lengkap, dari mula hingga akhir

Integrasi yang berfungsi hanya ada empat langkah. Contoh di bawah menjalankan kesemuanya pada API sebenar dalam cURL, Node dan PHP. Gantikan dengan kelayakan anda sendiri dan ia terus berjalan seperti yang ditulis.

  1. 1

    Baca katalog

    Telusuri dari kategori besar ke kategori, kemudian ke subkategori, lalu baca harga semasa item yang anda mahu jual. Simpan pokok kategori dalam cache, tetapi ambil harga item yang terkini setiap kali.

  2. 2

    Sahkan pemain

    Dapatkan nama dalam game dan region bagi ID tersebut sebelum sebarang wang berpindah. Di sinilah salah taip dan salah region dapat dikesan.

  3. 3

    Buat pesanan

    Jana UUID, simpan UUID itu, kemudian hantar sebagai order_id. UUID ini ialah kunci idempotensi anda dan satu-satunya cara yang selamat untuk menghantar semula permintaan untuk membuat pesanan.

  4. 4

    Sahkan penghantaran

    Tunggu webhook status pesanan. Bagi pesanan yang masih menunggu selepas tempoh timeout anda sendiri, baca statusnya terus.

cURL - aliran penuhTunjuk kod
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"
Aliran penuh (Node.js)Tunjuk kod
// 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 - aliran penuhTunjuk kod
<?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'];

Dua perkara ini patut disalin tepat seperti asal: simpan UUID pesanan sebelum panggilan buat pesanan, bukan selepasnya, dan baca semula harga daripada bergantung pada angka dalam cache. Dua tabiat ini menghapuskan hampir setiap caj berganda dan setiap penolakan PRICE_INCREASED.

Webhook: callback status pesanan

Daftar satu endpoint HTTPS dan platform akan menghantar keputusan pesanan ke situ sebaik ia berlaku, jadi storefront anda tidak perlu lagi polling dan boleh terus bertindak balas. Setiap callback ditandatangani supaya anda boleh membuktikan payload itu datang daripada kami sebelum mengambil tindakan.

Peristiwa

PeristiwaDicetuskan apabila
order.completedSatu unit dalam pesanan telah dihantar. Semak data.status untuk status keseluruhan pesanan.
order.refundedSatu unit daripada pesanan telah dibayar balik ke dompet anda. Nilai data.status bertukar kepada refunded setelah semua unit dibayar balik.
webhook.testAnda mencetuskan penghantaran ujian bertandatangan dari panel untuk menyemak handler anda.

Header penghantaran

Nama headerNilai
Content-Typeapplication/json
X-Shop2Topup-Signaturesha256=<hex>: HMAC-SHA256 bagi badan permintaan mentah
X-Shop2Topup-EventNama event, contohnya order.completed (pesanan selesai)
User-Agentshop2topup-webhook/1.0

Payload callback

Setiap callback ada tiga medan peringkat atas yang sama: event, timestamp dan data. Medan data mencerminkan objek pesanan yang dipulangkan oleh endpoint status. Kod baucar hanya ada pada pesanan yang selesai, player_name boleh jadi null, dan sub_transaction_summary muncul setiap kali pesanan dipecahkan kepada unit.

Kandungan POST order.completed (pesanan selesai)
{
  "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
    }
  }
}

Pengesahan tandatangan

Setiap penghantaran disertakan header X-Shop2Topup-Signature, iaitu sha256= diikuti nilai hex HMAC-SHA256 bagi body permintaan mentah, dikira dengan secret webhook anda. Sahkan terhadap bait mentah sebelum JSON dihurai, dan buat perbandingan dalam masa malar (constant time). Handler yang menghurai dahulu lalu mengesahkan kemudian boleh ditipu dengan body yang telah disiri semula.

Contoh Node.js: sahkan dan akui penerimaan
// 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);
  },
);

Penghantaran dan cubaan semula

Callback dicuba sekali sahaja dan endpoint anda diberi sepuluh saat untuk menjawab. Tiada cubaan semula automatik dan tiada exponential backoff, dan ini memang disengajakan: endpoint status ialah rekod muktamad, jadi penyelarasan patut dibuat melalui poll yang anda kawal, bukan bergantung pada jadual cubaan semula yang anda tidak nampak. Balas 200 serta-merta dan proses kerja itu dalam queue anda sendiri.

Handler idempoten

Gunakan order_id sebagai kunci handler anda dan pastikan callback yang dihantar semula tidak memberi apa-apa kesan. Walaupun penghantaran hanya dicuba sekali, infrastruktur anda sendiri sekali-sekala akan menyerahkan mesej yang sama kepada dua worker. Pesanan yang mengkreditkan pelanggan dua kali jauh lebih teruk daripada pesanan yang lambat dikreditkan.

Pendaftaran dan secret

Pendaftaran URL callback, penghantaran callback ujian dengan tandatangan digital dan penukaran kunci rahsia penandatanganan semuanya dibuat dalam panel reseller selepas anda log masuk, bukan melalui API awam. HTTPS wajib digunakan dan alamat persendirian ditolak.

Buka akses API di panel reseller

Had kadar dan pengendalian ralat

Had ini diterbitkan, dikira bagi setiap akaun dan dikuatkuasakan dengan tetingkap bergerak (rolling window). Kerja katalog yang banyak membaca mendapat had paling longgar. Endpoint status pula sengaja diketatkan kerana ia wujud sebagai sandaran penyelarasan, bukan untuk polling berterusan.

LaluanPermintaanTempohDikira mengikut
GET /catalog/* (kelima-lima)500060 saatsetiap akaun
POST /orders/create500060 saatsetiap akaun
GET /orders/:orderId360 saatsetiap akaun + ID pesanan
POST /orders/batch260 saatsetiap akaun
GET /orders260 saatsetiap akaun
GET /account6060 saatsetiap akaun
POST /player/validatekuota semakan pemain harian24 jamsetiap akaun (tiada header X-RateLimit)

Endpoint katalog, pesanan dan akaun menyertakan X-RateLimit-Limit, X-RateLimit-Remaining dan X-RateLimit-Reset pada setiap respons, jadi klien yang tertib tidak perlu meneka kedudukannya. Pantau nilai Remaining dan perlahankan permintaan sebelum dihadkan, bukan selepasnya. Pengesahan pemain ialah pengecualian: ia menggunakan kuota semakan pemain harian anda, tidak menghantar header X-RateLimit, dan melaporkan kuota itu dalam badan respons 429.

Respons dihadkan - 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
    }
  }
}

Ralat awal dan cara mengatasinya

Kegagalan sentiasa memulangkan kod tetap bersama status HTTP. Berikut ialah ralat yang biasa ditemui integrasi baharu dalam minggu pertama, setiap satu dengan perubahan yang menyelesaikannya.

KodStatus HTTPMaksudCara mengatasi
INVALID_API_KEY401Kunci API tidak sahKunci API yang diberikan tidak sepadan dengan mana-mana kunci aktif dalam sistem.Pastikan ID kunci dan secret anda betul. Jana semula kunci di panel API Access jika perlu.action: FIX_INPUT · retryable: false · retry_after: null
IP_NOT_ALLOWED403Alamat IP tiada dalam senarai dibenarkanPermintaan ini datang daripada alamat IP yang tiada dalam senarai IP yang dibenarkan untuk kunci API anda.Tambah IP server anda ke senarai dibenarkan di panel API Access, atau buang sekatan IP.action: FIX_INPUT · retryable: false · retry_after: null
MISSING_REQUIRED_FIELD400Medan wajib tiadaSatu medan wajib tiada dalam badan permintaan. Untuk keperluan pemain, details.field menyatakan medan yang tiada.Semak dokumentasi endpoint serta GET /catalog/category/:categoryId/requirements untuk medan pemain. Kemudian hantar semua medan wajib.action: FIX_INPUT · retryable: false · retry_after: null
INVALID_UUID_FORMAT400Format UUID tidak sahorder_id bukan dalam format UUID yang sah.Gunakan mana-mana format UUID yang sah (v1, v4, v7 dan lain-lain), iaitu xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.action: FIX_INPUT · retryable: false · retry_after: null
PLAYER_NOT_FOUND400Pemain tidak dijumpaiGame telah mengesahkan bahawa ID pemain ini tidak wujud. Kod ini hanya dipulangkan apabila jawapannya disahkan. Jika ID tidak dapat disemak, anda akan menerima PLAYER_CHECK_UNAVAILABLE.Pastikan ID pemain betul. Semak jika game ini memerlukan zone_id.action: FIX_INPUT · retryable: false · retry_after: null
PLAYER_CHECK_UNAVAILABLE503ID pemain untuk game ini tidak dapat disemak sekarangSemakan ID pemain untuk game ini tidak dapat dibuat pada masa ini. ID pemain tersebut TIDAK ditolak.Hantar permintaan yang sama sekali lagi selepas retry_after saat. Jangan beritahu pelanggan anda bahawa ID itu salah.action: RETRY_LATER · retryable: true · retry_after: 60
REGION_MISMATCH400Produk ini tidak boleh dihantar ke region pemain iniProduk ini tidak boleh dihantar ke region pemain tersebut. Ralat ini menyertakan player_region dan player_name.Pilih produk untuk region pemain tersebut.action: CHOOSE_OTHER · retryable: false · retry_after: null
INSUFFICIENT_BALANCE400Baki dompet tidak mencukupiBaki dompet anda tidak cukup untuk membayar pesanan ini.Top up dompet anda di halaman Reload sebelum membuat pesanan.action: TOP_UP · retryable: false · retry_after: null
OUT_OF_STOCK400Stok produk habisProduk yang diminta kehabisan stok buat masa ini.Cuba lagi nanti atau pilih produk lain.action: RETRY_LATER · retryable: true · retry_after: null
PRODUCT_UNAVAILABLE409Produk ini tidak tersedia buat sementara waktuProduk ini wujud tetapi tidak boleh dijual pada masa ini.Cuba lagi nanti, atau pilih produk lain.action: RETRY_LATER · retryable: true · retry_after: null
DUPLICATE_ORDER409ID pesanan penduaPesanan dengan UUID ini sudah wujud. UUID yang sama tidak boleh digunakan untuk membuat pesanan sekali lagi.Jana UUID baharu untuk pesanan baharu. Jika ralat ini muncul semasa cubaan semula, cubaan pertama sudah diterima: semak pesanan tersebut menggunakan GET /orders/{orderId}, bukan cipta pesanan lain.action: CHECK_ORDER · retryable: false · retry_after: null
PRICE_INCREASED400Harga telah naik melebihi jangkaanHarga seunit semasa lebih tinggi daripada expected_unit_price yang anda berikan.Dapatkan harga terkini dengan GET /catalog/subcategory/:id/price dan kemas kini expected_unit_price.action: FIX_INPUT · retryable: false · retry_after: null
RATE_LIMIT_EXCEEDED429Terlalu banyak permintaanAnda telah melebihi had untuk endpoint ini. Bagi bucket per minit (catalog, orders, account), setiap respons menyertakan X-RateLimit-Limit, X-RateLimit-Remaining dan X-RateLimit-Reset. Medan details turut memaparkan limit, remaining (0), retry_after dan window_seconds. POST /player/validate pula menggunakan kuota semakan pemain harian anda: responsnya tidak menyertakan header X-RateLimit, dan respons 429 mengandungi limit, current, remaining, reset_at dan reset_in_seconds dalam error. Dalam kedua-dua kes, error.retry_after dan header Retry-After menyatakan tempoh menunggu dalam saat.Tunggu selama bilangan saat yang dinyatakan dalam retry_after atau header Retry-After sebelum mencuba semula. Untuk endpoint dengan had per minit, pantau X-RateLimit-Remaining bagi mengelakkan permintaan mencecah had.action: RETRY_LATER · retryable: true · retry_after: saat sebelum set semula
INTERNAL_ERROR500Ralat dalaman serverBerlaku ralat yang tidak dijangka pada server.Hantar semula permintaan selepas retry_after saat. Untuk POST /orders/create, guna semula order_id yang sama supaya pesanan tidak akan dicipta dua kali. Jika masalah berterusan, hubungi sokongan.action: RETRY_LATER · retryable: true · retry_after: 30
Lihat rujukan penuh kod ralat: kesemua 42 kod berserta penyelesaiannya

Soalan integrasi dan jawapannya

Soalan yang benar-benar ditanya oleh pembangun semasa memasang integrasi ini, termasuk pengesahan, cubaan semula, idempotensi, had kadar, callback dan pengendalian kegagalan.

Bagaimana API reseller mengesahkan permintaan?

Dengan satu header HTTP: Authorization: Bearer <keyId>.<secret>. Pasangan kunci dikeluarkan dari halaman API Access dalam panel reseller anda, secret dipaparkan sekali sahaja semasa dicipta, dan server menyemaknya dengan HMAC pada setiap panggilan. Tiada redirect OAuth, tiada cookie sesi dan tiada token untuk diperbaharui, jadi header yang sama berfungsi untuk permintaan pertama dan setiap permintaan selepasnya.

Base URL dan versi API mana yang perlu digunakan klien saya?

Hanya ada satu URL asas, iaitu URL untuk persekitaran live. Tiada hos sandbox berasingan untuk pembangunan yang perlu ditukar kemudian. Halakan klien ke URL asas v1 di atas, hantar Authorization: Bearer <keyId>.<secret> pada setiap permintaan, dan mulakan dengan GET /account untuk mengesahkan kunci serta format header bagi akses akaun sebelum meneruskan. POST /orders/create ialah satu-satunya panggilan yang menggerakkan wang melalui penciptaan pesanan, jadi uji panggilan ini paling akhir menggunakan denominasi terkecil anda. Sertakan expected_unit_price sebagai harga seunit yang dijangka semasa klien masih dalam peringkat ujian.

Medan mana yang membawa idempotency key pada POST /orders/create?

order_id, iaitu UUID yang anda jana di sisi klien dan hantar dalam body create. Hantar semula create dengan order_id yang sudah wujud, dan API akan menjawab HTTP 409 dengan DUPLICATE_ORDER tanpa membuka pesanan kedua, jadi cubaan semula tidak akan jadi caj kedua; baca pesanan asal dengan GET /orders/:orderId. Jana dan simpan UUID sebelum cubaan pertama, kerana UUID yang dijana selepas timeout ialah kunci baharu, bukan kunci cubaan semula, dan server tidak akan menggabungkan dua nilai order_id walaupun maksudnya sama.

Permintaan saya timeout. Adakah pesanan sudah dibuat?

Semak status, jangan meneka. Panggil GET /orders/:orderId dengan UUID yang sama yang anda hantar: jika pesanan ditemui, timeout berlaku selepas pesanan diterima dan anda tidak perlu hantar semula. Jika anda dapat ORDER_NOT_FOUND, tiada caj dikenakan dan permintaan create boleh diulang dengan selamat menggunakan UUID yang sama.

Bagaimana saya sahkan tandatangan pada callback webhook?

Kira semula HMAC ke atas raw body permintaan dan bandingkan dengan header X-Shop2Topup-Signature. Header itu mengandungi sha256=<hex>: buat HMAC-SHA256 ke atas bait tepat yang anda terima menggunakan webhook secret anda, tambah awalan sha256=, dan bandingkan dengan semakan timing-safe. Jangan bandingkan dengan objek JSON yang disirikan semula, kerana parse dan stringify mengubah bait dan perbandingan tidak akan sepadan. Tolak yang tidak sepadan dengan 401, jawab penghantaran yang sah dengan 200 dalam masa sepuluh saat, dan uruskan penyelesaian dalam queue anda sendiri selepas itu.

Apa berlaku apabila saya mencapai had kadar?

Anda akan menerima HTTP 429 dengan kod RATE_LIMIT_EXCEEDED dan header Retry-After. Badan respons mengulang angka yang sama dalam retry_after serta menambah limit, remaining dan window_seconds. Tunggu tepat selama bilangan saat itu dan cuba semula sekali; jangan cuba semula serta-merta dan jangan pecahkan panggilan yang sama kepada beberapa kunci, kerana kiraan dihadkan pada akaun, bukan pada sambungan.

Medan apa yang dipulangkan oleh POST /player/validate?

player_name, berserta zone_id dan region jika game menyediakannya, di dalam objek data respons. Proses buat pesanan sendiri akan mengenal pasti dan menyemak region pemain, dan cubaan rentas region ditolak dengan REGION_MISMATCH (HTTP 400) sebelum dompet didebit, jadi panggilan ini bukan pelindung baki anda. Gunakannya kerana membaca semula player_name ialah satu-satunya cara praktikal untuk menunjukkan kepada pembeli akaun yang akan menerima top up dan mengesan ID yang salah taip sebelum mereka teruskan. Nama itu sentiasa datang daripada game, bukan daripada apa yang ditaip pembeli.

Bagaimana saya petakan respons requirements ke dalam payload pesanan?

Gunakan field_name sebagai kunci: setiap entri yang dipulangkan oleh GET /catalog/category/:categoryId/requirements menjadi satu property dalam objek requirements yang anda hantar ke POST /orders/create. Setiap entri juga membawa data_type, jadi borang anda tahu apa yang perlu dipaparkan dan server anda tahu jenis yang perlu ditukar, manakala entri single_select dan multi_select datang bersama nilai yang dibenarkan. Paparkan pilihan itu, bukan kotak teks bebas. Cache senarai bagi setiap kategori, jangan baca untuk setiap pesanan, dan baca semula apabila panggilan create menerima MISSING_REQUIRED_FIELD.

Bagaimana klien saya perlu mengendalikan respons PRICE_INCREASED?

Baca semula harga, buat keputusan, kemudian hantar semula. Jangan ulang body yang sama. PRICE_INCREASED (HTTP 400) bermaksud harga seunit semasa lebih tinggi daripada expected_unit_price yang anda hantar, jadi create ditolak dan tiada caj dikenakan; butiran ralat membawa kedua-dua expected_unit_price dan current_unit_price, dan cubaan semula dengan angka lama akan sentiasa ditolak. Baca GET /catalog/subcategory/:itemId/price, nilai sama ada harga baharu masih sesuai, dan hantar semula create dengan expected_unit_price yang dikemas kini serta order_id yang sama, kerana cubaan yang ditolak tidak menggunakannya. Tanpa expected_unit_price, perlindungan ini dimatikan sepenuhnya, dan itu hampir tidak pernah dimahukan oleh integrasi automatik.

Bagaimana pesanan yang dihantar sebahagian dipaparkan dalam respons pesanan?

Sebagai status partial, dengan pecahan diperincikan dalam sub_transaction_summary. Objek itu mengira unit sebagai completed, refunded atau pending, dan ketiga-tiganya sentiasa berjumlah total, manakala sub_transactions menyenaraikan setiap unit dengan status masing-masing. Unit yang belum dihantar atau dibayar balik kekal pending. Anggap pending sebagai belum muktamad dan terus tunggu; anggap completed, partial dan refunded sebagai muktamad. Pembilang failed, processing dan retrying dikekalkan hanya untuk keserasian dan sentiasa 0. Callback order.completed dan order.refunded membawa ringkasan yang sama, jadi payload yang disahkan boleh diselesaikan tanpa bacaan kedua.

Apa yang diterima oleh permintaan dari alamat di luar allowlist?

HTTP 403 dengan kod IP_NOT_ALLOWED, dipulangkan sebelum sebarang logik perniagaan dijalankan. Semakan ini berlaku apabila kunci mempunyai IP allowlist yang tidak kosong, jadi kunci yang bocor tidak berguna di mana-mana kecuali alamat egress anda sendiri. Buat cabang berdasarkan error.code, bukan teks mesej. IP_NOT_ALLOWED yang tiba-tiba muncul dalam production biasanya tanda alamat egress sudah berubah, contohnya NAT gateway atau subnet worker baharu, bukan kunci yang rosak. Biarkan allowlist kosong hanya jika alamat keluar anda memang tidak tetap.

Berapa lama masa untuk pesanan selesai?

Anggap penyelesaian sebagai asinkron, bukan serta-merta. POST /orders/create memberi respons sebaik pesanan diterima dan dompet didebit, biasanya dengan status pending; penghantaran kemudian dijalankan oleh worker berasingan dan status akhir sampai kepada anda melalui webhook atau pada bacaan status seterusnya. Jangan tahan permintaan HTTP pelanggan sementara menunggu status akhir. Terima pesanan, beri jawapan kepada pelanggan, dan selesaikan di luar aliran itu.

Buat akaun, jana kunci, terus beroperasi

Tiada giliran kelulusan dan tiada tempoh menunggu antara pendaftaran dan panggilan API pertama. Buat akaun reseller, jana kunci di halaman Akses API, top up dompet, dan pesanan pertama anda boleh dihantar pada hari yang sama.

Jika anda bukan pembangun projek ini, bahagian komersial diterangkan di halaman program reseller SHOP2TOPUP.

API Top Up Game & Baucar untuk Reseller | Dokumentasi REST API