REST API - versi 1
API Top Up Game & Voucher untuk Reseller
Satu REST API untuk top up game, isi ulang game, dan kode voucher: baca katalog, validasi pemain, buat pesanan, lalu terima hasilnya lewat webhook atau polling status.
Ini adalah API top up host-to-host (H2H) di balik SHOP2TOPUP. Bentuknya API JSON biasa lewat HTTPS dengan autentikasi Bearer, idempotency key berupa UUID, rate limit yang dipublikasikan, dan kode error yang stabil serta bisa dibaca mesin. Dengan begitu, toko, storefront, atau bot bisa menjual kredit game digital secara terprogram tanpa client library. Antarmuka yang sama sudah mengantarkan 3,000,000+ pesanan selama 5+ tahun beroperasi terus-menerus.
Info soal harga, metode pembayaran, dan cara kerja akun partner ada di ringkasan program reseller SHOP2TOPUP.
Request pertama, sebelum kamu menandatangani apa pun
Setiap endpoint cuma request HTTPS biasa dengan satu header. Tidak ada SDK yang perlu dipasang, client library yang perlu dibundel, atau langkah build tambahan. Tempel perintah di bawah, masukkan key kamu, dan respons katalog live langsung muncul di terminal.
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"
}
]
}URL dasar
https://shop2topup.com/api/endpoints/v1
Semua path di dokumentasi ini relatif terhadap URL dasar tersebut. Respons berupa JSON dan selalu memuat field boolean success, jadi satu percabangan di client kamu cukup untuk memisahkan alur sukses dari kasus lainnya.
Format respons error
Respons gagal punya bentuk yang sama di setiap status code. Buat percabangan berdasarkan error.code, karena itu bagian dari kontrak. Jangan pernah bercabang berdasarkan error.message: teks itu ditulis untuk manusia dan bisa diubah kapan saja.
{
"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"
}
}
}Semua endpoint API dalam satu layar
Sebelas endpoint sudah mencakup seluruh integrasi: baca katalog, cek harga item, periksa pemain, buat pesanan, lalu rekonsiliasi setelahnya. Setiap baris langsung tertaut ke referensi lengkap yang berisi semua parameter, contoh request, dan contoh respons.
| Metode | Jalur | Fungsi | Referensi |
|---|---|---|---|
| GET | /account | Ambil Info Akun | Baca referensi |
| GET | /catalog/big-categories | Daftar Kategori Utama | Baca referensi |
| GET | /catalog/categories | Daftar Kategori | Baca referensi |
| GET | /catalog/subcategories | Daftar Subkategori (Produk) | Baca referensi |
| GET | /catalog/subcategory/:itemId/price | Ambil Harga Item | Baca referensi |
| GET | /catalog/category/:categoryId/requirements | Ambil Syarat Kategori | Baca referensi |
| POST | /player/validate | Validasi Pemain | Baca referensi |
| POST | /orders/create | Buat pesanan | Baca referensi |
| GET | /orders/:orderId | Cek Status Pesanan | Baca referensi |
| POST | /orders/batch | Ambil pesanan sekaligus | Baca referensi |
| GET | /orders | Daftar Pesanan | Baca referensi |
Ditambah webhook status pesanan, yang dijelaskan lengkap di bagian bawah halaman ini.
Autentikasi dan kontrol akses
Autentikasi cukup lewat satu header HTTP. Setiap request membawa header Authorization berisi kredensial Bearer yang terdiri dari key ID dan secret, diterbitkan dari halaman API Access di panel reseller kamu. Key ID menunjukkan akunnya, sedangkan secret membuktikan akun itu milikmu dan diverifikasi di server dengan pengecekan HMAC.
Secret hanya ditampilkan sekali, saat dibuat, dan tidak bisa diambil lagi setelahnya. Simpan di secret manager yang sudah dipakai stack kamu, masukkan sebagai environment variable, dan jangan sampai masuk ke source control. Kalau hilang, solusinya rotasi key, bukan pemulihan.
Tidak ada redirect OAuth, cookie sesi, refresh token, atau apa pun yang perlu dijadwalkan. Ini penting untuk integrasi otomatis: cron job atau queue worker bisa memakai kredensial yang sama selama bertahun-tahun tanpa proses perpanjangan, dan kamu tidak akan dibangunkan jam tiga pagi gara-gara kredensial kedaluwarsa.
Kamu bisa memasang IP allowlist pada key. Begitu allowlist terisi, request dari alamat lain ditolak dengan IP_NOT_ALLOWED dan HTTP 403 sebelum logika bisnis apa pun berjalan, jadi kredensial yang bocor tidak berguna di luar server milikmu. Rotasi key cukup lewat deploy, bukan migrasi: buat key baru, deploy, lalu hapus yang lama.
Setiap key terikat ke satu akun reseller dan memakai dompet akun itu, jadi jangan pernah menaruh secret di browser, bundle aplikasi mobile, atau repository publik. Panggil API dari backend, dan biarkan front end kamu berkomunikasi dengan backend tersebut. Semua yang didokumentasikan di sini mengasumsikan pemanggil server-to-server.
Authorization: Bearer <keyId>.<secret>Cek key dengan satu request
GET /account berfungsi sebagai health check untuk kredensial. Endpoint ini menjawab tiga hal sekaligus: apakah key lolos autentikasi, apakah akun aktif, dan apakah saldo dompet cukup untuk pesanan yang akan kamu buat. Jalankan saat aplikasi start dan sebelum setiap batch.
Baca referensi autentikasi dan akunIntegrasi lengkap dari awal sampai akhir
Integrasi yang berjalan cuma butuh empat langkah. Contoh di bawah menjalankan semuanya ke API live dalam cURL, Node, dan PHP. Ganti dengan kredensial kamu sendiri, dan contohnya bisa langsung dijalankan apa adanya.
- 1
Baca katalog
Telusuri dari kategori besar ke kategori, lalu ke subkategori, kemudian ambil harga terkini untuk item yang akan kamu jual. Simpan struktur katalog di cache, tapi harga item selalu ambil yang baru.
- 2
Validasi pemain
Cocokkan ID dengan nama in-game dan region sebelum ada uang yang bergerak. Di tahap ini, salah ketik dan salah region bisa ketahuan.
- 3
Buat pesanan
Buat UUID, simpan, lalu kirim sebagai order_id. UUID ini adalah idempotency key kamu, dan satu-satunya cara aman untuk mengulang request create.
- 4
Konfirmasi pengiriman
Tunggu webhook status pesanan. Kalau masih pending setelah batas timeout yang kamu tentukan, cek statusnya lewat API sebagai cadangan.
cURL - alur lengkapTampilkan kodeSembunyikan kode
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"Alur lengkap Node.jsTampilkan kodeSembunyikan kode
// 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 - alur lengkapTampilkan kodeSembunyikan kode
<?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 detail ini layak kamu salin apa adanya: simpan UUID pesanan sebelum memanggil create, bukan sesudahnya, lalu baca ulang harganya daripada mengandalkan angka dari cache. Dua kebiasaan itu menghilangkan hampir semua tagihan ganda dan penolakan PRICE_INCREASED.
Webhook: callback status pesanan
Daftarkan satu endpoint HTTPS, lalu platform akan mengirim hasil pesanan ke sana begitu terjadi. Toko kamu tidak perlu polling lagi dan cukup merespons. Callback-nya ditandatangani, jadi kamu bisa memastikan payload benar dari kami sebelum memprosesnya.
Daftar event
| Nama event | Dipicu saat |
|---|---|
| order.completed | Satu unit pesanan sudah terkirim. Cek data.status untuk status pesanan secara keseluruhan. |
| order.refunded | Satu unit pesanan sudah di-refund ke dompet kamu. Nilai data.status berubah jadi refunded setelah semua unit di-refund. |
| webhook.test | Kamu memicu pengiriman tes bertanda tangan dari panel untuk mengecek handler-mu. |
Header pengiriman
| Nama header | Nilai |
|---|---|
| Content-Type | application/json |
| X-Shop2Topup-Signature | sha256=<hex>: HMAC-SHA256 dari body request mentah |
| X-Shop2Topup-Event | Nama event, contoh: order.completed (pesanan selesai) |
| User-Agent | shop2topup-webhook/1.0 |
Payload callback
Setiap callback punya tiga field tingkat atas yang sama: event, timestamp dan data. Isi data sama persis dengan objek pesanan yang dikembalikan endpoint status. Kode Voucher hanya ada di pesanan yang sudah selesai, player_name bisa bernilai null, dan sub_transaction_summary (ringkasan transaksi per unit) selalu muncul jika pesanan dipecah menjadi beberapa unit.
{
"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
}
}
}Verifikasi signature
Setiap pengiriman membawa header X-Shop2Topup-Signature berisi sha256= diikuti HMAC-SHA256 dalam format hex dari body request mentah, dihitung dengan webhook secret kamu. Verifikasi terhadap byte mentah sebelum JSON di-parse, dan bandingkan secara constant-time. Handler yang mem-parse dulu baru memverifikasi bisa tertipu oleh body yang diserialisasi ulang.
// 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);
},
);Pengiriman dan retry
Callback hanya dicoba satu kali, dan endpoint kamu punya waktu sepuluh detik untuk menjawab. Tidak ada retry otomatis atau exponential backoff, dan itu memang disengaja. Endpoint status adalah catatan yang jadi acuan, jadi rekonsiliasi sebaiknya lewat polling yang kamu kendalikan, bukan jadwal retry yang tidak bisa kamu lihat. Langsung balas 200, lalu kerjakan prosesnya di antrean kamu sendiri.
Handler idempoten
Jadikan order_id sebagai kunci di handler kamu, dan pastikan callback yang diputar ulang tidak mengubah apa pun. Walaupun pengiriman cuma dicoba sekali, infrastruktur kamu sendiri sesekali akan menyerahkan pesan yang sama ke dua worker. Pesanan yang mengkreditkan pelanggan dua kali adalah bug yang jauh lebih parah daripada pesanan yang kreditnya terlambat.
Pendaftaran dan secret
Pendaftaran URL callback, pengiriman tes yang ditandatangani, dan rotasi signing secret semuanya dilakukan di panel reseller setelah kamu masuk, bukan lewat API publik. Wajib pakai HTTPS, dan alamat privat akan ditolak.
Buka Akses API di panel resellerRate limit dan penanganan error
Batas penggunaan dipublikasikan dan diterapkan per akun dengan perhitungan rolling window. Pengambilan data katalog mendapat kuota paling besar karena perlu sering dilakukan. Endpoint status sengaja dibatasi lebih ketat karena fungsinya sebagai cadangan untuk rekonsiliasi, bukan untuk polling terus-menerus.
| Rute | Jumlah request | Rentang waktu | Dihitung per |
|---|---|---|---|
| GET /catalog/* (kelimanya) | 5000 | 60 detik | per akun |
| POST /orders/create | 5000 | 60 detik | per akun |
| GET /orders/:orderId | 3 | 60 detik | per akun + ID pesanan |
| POST /orders/batch | 2 | 60 detik | per akun |
| GET /orders | 2 | 60 detik | per akun |
| GET /account | 60 | 60 detik | per akun |
| POST /player/validate | kuota cek pemain harian | 24 jam | per akun (tanpa header X-RateLimit) |
Endpoint katalog, pesanan, dan akun menyertakan X-RateLimit-Limit, X-RateLimit-Remaining, dan X-RateLimit-Reset di setiap respons, jadi client yang tertib tidak perlu menebak posisinya. Pantau sisa kuota dan kurangi kecepatan sebelum kena throttle, bukan setelahnya. Validasi pemain jadi pengecualian: fitur ini memakai kuota cek pemain harianmu, tidak mengirim header X-RateLimit, dan menampilkan kuota itu di body respons 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
}
}
}Error pertama yang akan kamu temui dan cara mengatasinya
Setiap kegagalan selalu mengembalikan kode error yang konsisten beserta status HTTP. Berikut error yang biasa ditemui pada minggu pertama integrasi baru, masing-masing disertai perubahan yang perlu dilakukan untuk mengatasinya.
| Kode | Status HTTP | Artinya | Cara mengatasinya |
|---|---|---|---|
| INVALID_API_KEY | 401 | API key tidak validAPI key yang dikirim tidak cocok dengan key aktif mana pun di sistem. | Pastikan key ID dan secret kamu sudah benar. Kalau perlu, buat ulang key lewat panel API Access.action: FIX_INPUT · retryable: false · retry_after: null |
| IP_NOT_ALLOWED | 403 | Alamat IP tidak ada di allowlistRequest dikirim dari alamat IP yang tidak terdaftar di allowlist API key kamu. | Tambahkan IP server kamu ke allowlist di panel API Access, atau hapus pembatasan IP.action: FIX_INPUT · retryable: false · retry_after: null |
| MISSING_REQUIRED_FIELD | 400 | Field wajib belum diisiAda field wajib yang tidak ada di body request. Untuk data pemain yang dibutuhkan, details.field menyebutkan field yang hilang. | Cek dokumentasi endpoint (untuk field pemain, cek juga GET /catalog/category/:categoryId/requirements), lalu kirim semua field wajib.action: FIX_INPUT · retryable: false · retry_after: null |
| INVALID_UUID_FORMAT | 400 | Format UUID tidak validorder_id bukan format UUID yang valid. | Pakai UUID dengan format yang valid (v1, v4, v7, dan lainnya), yaitu xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.action: FIX_INPUT · retryable: false · retry_after: null |
| PLAYER_NOT_FOUND | 400 | Pemain tidak ditemukanPihak game sudah memastikan bahwa ID pemain ini tidak ada. Kode ini hanya dikirim kalau jawabannya sudah pasti. Kalau ID tidak bisa dicek, yang kamu terima adalah PLAYER_CHECK_UNAVAILABLE. | Pastikan ID pemain sudah benar. Cek juga apakah game ini butuh zone_id.action: FIX_INPUT · retryable: false · retry_after: null |
| PLAYER_CHECK_UNAVAILABLE | 503 | ID pemain untuk game ini belum bisa dicek sekarangUntuk saat ini, ID pemain di game ini tidak bisa dicek. ID pemain tersebut TIDAK ditolak. | Coba lagi request yang sama setelah retry_after detik. Jangan bilang ke pelanggan bahwa ID-nya salah.action: RETRY_LATER · retryable: true · retry_after: 60 |
| REGION_MISMATCH | 400 | Produk ini tidak bisa dikirim ke pemain di region tersebutPemain berada di region yang tidak bisa dikirimi produk ini. Error ini menyertakan player_region dan player_name. | Pilih produk yang sesuai dengan region pemain.action: CHOOSE_OTHER · retryable: false · retry_after: null |
| INSUFFICIENT_BALANCE | 400 | Saldo dompet tidak cukupDana di dompet kamu kurang untuk menyelesaikan pesanan ini. | Top up dompet kamu dulu lewat halaman Isi Ulang Dompet sebelum membuat pesanan.action: TOP_UP · retryable: false · retry_after: null |
| OUT_OF_STOCK | 400 | Stok produk habisStok produk yang kamu minta sedang habis. | Coba lagi nanti atau pilih produk lain.action: RETRY_LATER · retryable: true · retry_after: null |
| PRODUCT_UNAVAILABLE | 409 | Produk ini untuk sementara tidak tersediaProduknya ada, tapi belum bisa dijual saat ini. | Coba lagi nanti, atau ganti ke produk lain.action: RETRY_LATER · retryable: true · retry_after: null |
| DUPLICATE_ORDER | 409 | ID pesanan duplikatPesanan dengan UUID ini sudah ada. Pemeriksaan ini mencegah pesanan yang sama dibuat lebih dari sekali. | Buat UUID baru untuk pesanan baru. Kalau error ini muncul saat retry, artinya percobaan pertama sudah masuk: cek pesanan itu lewat GET /orders/{orderId}, jangan buat pesanan baru.action: CHECK_ORDER · retryable: false · retry_after: null |
| PRICE_INCREASED | 400 | Harga naik melebihi perkiraanHarga satuan saat ini lebih tinggi dari expected_unit_price yang kamu kirim. | Ambil harga terbaru lewat GET /catalog/subcategory/:id/price, lalu perbarui expected_unit_price.action: FIX_INPUT · retryable: false · retry_after: null |
| RATE_LIMIT_EXCEEDED | 429 | Rate limit terlampauiKamu sudah melewati batas untuk endpoint ini. Untuk bucket per menit (catalog, orders, account), setiap respons menyertakan header X-RateLimit-Limit, X-RateLimit-Remaining dan X-RateLimit-Reset. Bagian details juga memuat limit, remaining (0), retry_after dan window_seconds. Khusus POST /player/validate, batas penggunaan mengikuti kuota cek pemain harian kamu. Endpoint ini tidak mengirim header X-RateLimit. Respons 429-nya memuat limit, current, remaining, reset_at dan reset_in_seconds di dalam error. Pada kedua jenis batas ini, error.retry_after dan header Retry-After berisi waktu tunggu dalam detik. | Tunggu selama jumlah detik yang tercantum di retry_after atau header Retry-After sebelum mencoba lagi. Untuk endpoint dengan batas per menit, pantau X-RateLimit-Remaining agar tidak melebihi batas.action: RETRY_LATER · retryable: true · retry_after: detik lagi sampai reset |
| INTERNAL_ERROR | 500 | Kesalahan server internalTerjadi kesalahan tak terduga di server. | Kirim ulang request setelah retry_after detik. Untuk POST /orders/create, pakai order_id yang sama supaya pesanan tidak pernah dibuat dua kali. Kalau error masih muncul, hubungi tim support.action: RETRY_LATER · retryable: true · retry_after: 30 |
Tanya jawab seputar integrasi
Pertanyaan yang benar-benar diajukan developer saat menyambungkan integrasi ini: autentikasi, retry, idempotensi, throttling, callback, dan penanganan kegagalan.
Bagaimana API reseller mengautentikasi request?
Cukup dengan satu header HTTP: Authorization: Bearer <keyId>.<secret>. Pasangan key dibuat dari halaman API Access di panel reseller, secret hanya ditampilkan sekali saat dibuat, dan server memverifikasinya dengan pengecekan HMAC di setiap panggilan. Tidak ada redirect OAuth, cookie sesi, atau token yang perlu di-refresh, jadi header yang sama berlaku untuk request pertama dan semua request sesudahnya.
Base URL dan versi API mana yang harus dituju client?
Hanya ada satu base URL, yaitu yang live. Tidak ada host sandbox terpisah untuk dipakai saat develop lalu diganti belakangan. Arahkan client ke base URL v1 di atas, kirim Authorization: Bearer <keyId>.<secret> di setiap request, dan mulai dengan GET /account untuk memastikan key dan format header-nya benar sebelum yang lain. POST /orders/create adalah satu-satunya panggilan yang memindahkan dana, jadi taruh di urutan terakhir, coba dengan denominasi terkecilmu, dan sertakan expected_unit_price selama client masih dalam tahap uji coba.
Field apa yang berisi idempotency key di POST /orders/create?
order_id: UUID yang kamu buat sendiri di sisi client lalu kirim di body request create. Kalau request create dikirim ulang dengan order_id yang sudah ada, API membalas HTTP 409 dengan DUPLICATE_ORDER dan tidak membuka pesanan kedua, jadi retry tidak akan pernah jadi tagihan kedua. Pesanan aslinya bisa kamu baca lagi lewat GET /orders/:orderId. Buat dan simpan UUID sebelum percobaan pertama, karena UUID yang dibuat setelah timeout dihitung sebagai key baru, bukan key retry, dan server tidak akan menggabungkan dua nilai order_id meski maksud keduanya sama.
Request kena timeout. Apakah pesanannya sudah dibuat?
Cek statusnya, jangan menebak. Panggil GET /orders/:orderId dengan UUID yang sama yang kamu kirim. Kalau pesanannya muncul, berarti timeout terjadi setelah pesanan diterima, jadi jangan kirim ulang. Kalau yang keluar ORDER_NOT_FOUND, berarti belum ada saldo yang dipotong dan kamu aman mengulang request create dengan UUID yang sama.
Bagaimana cara memverifikasi signature di callback webhook?
Hitung ulang HMAC dari raw body request, lalu bandingkan dengan header X-Shop2Topup-Signature. Header itu berisi sha256=<hex>: hitung HMAC-SHA256 dari byte persis yang kamu terima memakai webhook secret milikmu, tambahkan prefix sha256=, lalu bandingkan dengan pengecekan timing-safe. Jangan pernah membandingkan dengan objek JSON yang sudah diserialisasi ulang, karena proses parse dan stringify mengubah byte-nya sehingga hasilnya tidak akan pernah cocok. Tolak yang tidak cocok dengan 401, balas pengiriman yang valid dengan 200 dalam sepuluh detik, lalu proses settlement di queue-mu sendiri setelahnya.
Apa yang terjadi kalau kena rate limit?
Kamu akan menerima HTTP 429 dengan kode RATE_LIMIT_EXCEEDED dan header Retry-After. Body respons mengulang angka yang sama di retry_after, ditambah limit, remaining, dan window_seconds. Tunggu tepat selama jumlah detik itu, lalu retry sekali. Jangan langsung retry, dan jangan sebar panggilan yang sama ke beberapa key, karena counter-nya dihitung per akun, bukan per koneksi.
Field apa saja yang dikembalikan POST /player/validate?
player_name, ditambah zone_id dan region kalau game-nya menyediakan data itu, di dalam objek data pada respons. Saat membuat pesanan, sistem sudah mengecek pemain dan region-nya sendiri, dan percobaan lintas region ditolak dengan REGION_MISMATCH (HTTP 400) sebelum saldo dompet dipotong. Jadi panggilan ini bukan yang melindungi saldomu. Gunakan panggilan ini karena membaca player_name adalah satu-satunya cara praktis untuk menunjukkan ke pembeli akun mana yang akan di-top up dan menangkap ID yang salah ketik sebelum mereka lanjut. Nama itu selalu diambil dari game, tidak pernah dari apa yang diketik pembeli.
Bagaimana cara memetakan respons requirements ke payload pesanan?
Pakai field_name sebagai key: setiap entri yang dikembalikan GET /catalog/category/:categoryId/requirements menjadi satu properti di objek requirements yang kamu kirim ke POST /orders/create. Tiap entri juga membawa data_type, jadi form-mu tahu apa yang harus ditampilkan dan server-mu tahu tipe apa untuk cast. Entri single_select dan multi_select sudah menyertakan nilai yang diizinkan, jadi tampilkan pilihan itu, bukan kolom teks bebas. Simpan daftarnya di cache per kategori, jangan dibaca ulang tiap pesanan, dan baca lagi kalau panggilan create dibalas MISSING_REQUIRED_FIELD.
Bagaimana aplikasi saya sebaiknya menangani respons PRICE_INCREASED?
Cek ulang harganya, putuskan, baru kirim lagi. Jangan mengirim ulang body yang sama. PRICE_INCREASED (HTTP 400) berarti harga satuan saat ini lebih tinggi dari expected_unit_price yang kamu kirim, jadi pembuatan pesanan ditolak dan tidak ada yang ditagih. Detail error memuat expected_unit_price sekaligus current_unit_price, dan retry asal kirim dengan angka lama akan selalu ditolak. Baca GET /catalog/subcategory/:itemId/price, putuskan apakah harga baru masih masuk buatmu, lalu kirim ulang create dengan expected_unit_price yang sudah diperbarui dan order_id yang sama. Percobaan yang ditolak tidak pernah memakai order_id itu. Kalau expected_unit_price tidak disertakan, pengaman ini mati total, dan hampir tidak ada integrasi otomatis yang menginginkan itu.
Seperti apa pesanan yang baru terkirim sebagian di respons pesanan?
Muncul dengan status partial, dan rinciannya ada di sub_transaction_summary. Objek itu menghitung unit sebagai completed, refunded, atau pending (jumlah ketiganya selalu sama dengan total), sedangkan sub_transactions mencantumkan setiap unit beserta statusnya sendiri. Unit yang belum terkirim atau belum di-refund tetap pending. Anggap pending belum final dan terus tunggu; completed, partial, dan refunded sudah final. Counter failed, processing, dan retrying hanya dipertahankan demi kompatibilitas dan nilainya selalu 0. Callback order.completed dan order.refunded membawa ringkasan yang sama, jadi payload yang sudah terverifikasi cukup untuk settlement tanpa membaca status lagi.
Apa respons untuk request dari alamat di luar allowlist?
HTTP 403 dengan kode IP_NOT_ALLOWED, dikirim sebelum logika bisnis apa pun berjalan. Pemeriksaan ini berlaku setiap kali key punya IP allowlist yang tidak kosong, sehingga key yang bocor tidak bisa dipakai di mana pun selain dari alamat egress milikmu. Buat percabangan berdasarkan error.code, bukan teks pesannya. Kalau IP_NOT_ALLOWED tiba-tiba muncul di production, anggap itu tanda alamat egress berubah (NAT gateway baru atau subnet worker baru), bukan key yang rusak. Biarkan allowlist kosong hanya kalau alamat outbound kamu memang tidak stabil.
Berapa lama pesanan sampai selesai?
Anggap penyelesaian pesanan berjalan asinkron, bukan instan. POST /orders/create langsung mengembalikan respons begitu pesanan diterima dan saldo dompet terpotong, biasanya dengan status pending. Setelah itu, pemenuhan pesanan diproses oleh worker terpisah, dan status finalnya sampai ke kamu lewat webhook atau saat kamu membaca status berikutnya. Jangan tahan request HTTP pelanggan sampai status final keluar. Terima pesanannya, balas pelangganmu, lalu selesaikan settlement di luar alur itu.
Buat akun, generate API key, lalu go live
Tidak ada antrean persetujuan atau masa tunggu dari daftar sampai memanggil API. Buat akun reseller, generate key di halaman Akses API, isi saldo dompet, dan pesanan pertamamu sudah bisa jalan hari itu juga.
Bukan developer di proyek ini? Sisi bisnisnya dibahas di halaman program reseller SHOP2TOPUP.