1. Ringkasan integrasi
| Informasi | Nilai |
|---|---|
| Endpoint utama | https://toctoico.com/chargingws/v2 |
| API daftar biaya | https://toctoico.com/chargingws/v2/getfee?partner_id=YOUR_PARTNER_ID |
| Method | POST |
| Tipe data | application/json atau application/x-www-form-urlencoded |
| Perintah yang didukung | charging, check |
2. Persyaratan sebelum memanggil API
Kompatibel dengan protokol Nappay: ubah URL ke website ini dan gunakan Partner ID/Key serta kode dompet Merchant di sini. Katalog, harga, dan ketentuan pemrosesan mengikuti sistem ini.
Integrasikan langsung ke sistem ini memakai Merchant akun Anda. Semua nominal dalam dong Vietnam (VND), kirim bilangan bulat tanpa pemisah.
- Login → Koneksi API / Merchant, buat Merchant untuk layanan yang sesuai lalu aktifkan.
- Ambil Partner ID, Partner Key, dan kode dompet dari Merchant tersebut. Jika menggunakan daftar IP yang diizinkan, daftarkan IP publik server pengirim request.
- Atur URL callback HTTPS pada sistem penerima hasil; pilih POST JSON atau GET sesuai Merchant.
- Ambil katalog/biaya dan periksa saldo terlebih dahulu; lalu integrasikan pembuatan pesanan, pencarian, dan rekonsiliasi.
Partner Key hanya digunakan di server Anda. Jangan menaruh secret dalam JavaScript browser, aplikasi publik, atau URL. YOUR_PARTNER_ID, YOUR_PARTNER_KEY, YOUR_WALLET di bawah merupakan tempat mengisi informasi Merchant Anda sendiri di web ini.
Endpoint penukaran kartu, top up, dan pembelian kode voucher
| Layanan | Endpoint | Format |
|---|---|---|
| Penukaran kartu | https://toctoico.com/chargingws/v2 | POST JSON atau form-urlencoded |
| Top up | https://toctoico.com/api/rechargews | POST JSON atau form-urlencoded |
| Beli kode voucher | https://toctoico.com/api/cardws | POST JSON atau form-urlencoded |
| Biaya penukaran kartu | https://toctoico.com/chargingws/v2/getfee?partner_id=YOUR_PARTNER_ID | GET |
| Katalog kode voucher | https://toctoico.com/api/cardws/products?partner_id=YOUR_PARTNER_ID | GET |
3. Tanda tangan autentikasi
Buat tanda tangan MD5 dari string UTF-8 yang digabung tepat sesuai urutan di bawah, tanpa titik, spasi, atau pemisah. Kirim string hex MD5; gunakan command dengan huruf kecil persis seperti dokumentasi.
| Grup perintah | String untuk hash MD5 |
|---|---|
| Tukar kartu: charging | partner_key + code + command + partner_id + request_id + serial + telco |
| Tukar kartu: check | partner_key + command + partner_id + request_id |
| Tukar kartu: tanda tangan lama tetap didukung | partner_key + code + serial |
| Topup: topup, getstatus | partner_key + partner_id + command + request_id |
| Topup: productlist, getbalance, feetopup, canceltopup | partner_key + partner_id + command |
| Kode voucher: buycard, redownload | partner_key + partner_id + command + request_id |
| Kode voucher: getbalance, checkavailable | partner_key + partner_id + command |
| Katalog kode voucher, getfee | Autentikasi Merchant/IP melalui partner_id; endpoint saat ini tidak memerlukan sign |
| Pencarian serial /api/checkcard | partner_key + serial |
hashlib.md5((partner_key + partner_id + command + request_id).encode("utf-8")).hexdigest()Setiap Merchant hanya memakai jenis API yang terdaftar (Charging, Recharge, Softcard, atau All). Error autentikasi/IP dapat muncul dalam JSON meskipun HTTP 200.
4. Parameter dan respons penukaran kartu
POST /chargingws/v2
Kirim kartu dengan command=charging. Periksa menggunakan command=check dengan partner_id, request_id, dan sign sesuai tabel di atas; tidak perlu mengirim ulang informasi kartu. Tanda tangan lama MD5(partner_key + code + serial) tetap didukung jika code dan serial disertakan.
| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| partner_id, sign | Wajib | Merchant dan tanda tangan layanan penukaran kartu |
| command | Wajib | charging atau check |
| request_id | Wajib | Kode permintaan unik per Merchant; check memakai ulang kode lama |
| telco | Khi charging | Kode operator huruf besar; ambil dari getfee |
| amount | Khi charging | Nominal yang dinyatakan; boleh diabaikan saat check |
| code, serial | Khi charging | String kode kartu/serial, pertahankan angka 0 di awal; check harus mengirim string awal yang persis sama |
{
"partner_id": "YOUR_PARTNER_ID",
"command": "charging",
"request_id": "CHARGE_DEMO_001",
"telco": "VIETTEL",
"amount": 100000,
"code": "YOUR_CARD_CODE",
"serial": "YOUR_CARD_SERIAL",
"sign": "MD5_THEO_CONG_THUC_CHARGING"
}Periksa kembali transaksi
{
"partner_id": "YOUR_PARTNER_ID",
"command": "check",
"request_id": "CHARGE_DEMO_001",
"sign": "MD5_PARTNER_KEY_CHECK_PARTNER_ID_REQUEST_ID"
}Respons transaksi
{
"trans_id": 123,
"request_id": "CHARGE_DEMO_001",
"status": 99,
"message": "PENDING",
"declared_value": 100000,
"value": 0,
"amount": 0,
"telco": "VIETTEL",
"code": "YOUR_CARD_CODE",
"serial": "YOUR_CARD_SERIAL",
"callback_sign": "MD5_PARTNER_KEY_CODE_SERIAL"
}| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| trans_id, request_id | Diterima | Kode transaksi sistem dan kode permintaan Anda |
| declared_value | Diterima | Nominal yang dinyatakan |
| value | Diterima | Nominal aktual terkonfirmasi |
| amount | Diterima | Jumlah bersih yang diterima dompet setelah pemrosesan |
| status, message | Diterima | Status kartu atau error; selalu periksa keduanya |
| status | Arti |
|---|---|
| 99 | Menunggu; belum dikreditkan seperti pesanan berhasil |
| 1 | Kartu valid, berhasil |
| 2 | Kartu valid tetapi nominal salah |
| 3 | Kartu gagal / tidak valid |
| 4 | Lewati / pemeliharaan berdasarkan hasil pemrosesan |
| 100 | Error permintaan |
| 101 | Transaksi tidak ditemukan |
| 102 | Autentikasi, IP, atau data salah |
| 104 | Perintah tidak valid |
Daftar biaya per Merchant
GET /chargingws/v2/getfee?partner_id=YOUR_PARTNER_ID. Respons berupa daftar datar berisi telco, value, fees, dan penalty, kompatibel dengan Nappay. Integrasi lama yang memerlukan object berkelompok dapat menambahkan format=grouped. fees adalah biaya pelanggan sesuai grup akun; hanya jenis kartu/nominal yang aktif yang dikembalikan. Jangan menetapkan katalog atau biaya secara permanen berdasarkan contoh dokumentasi.
[
{
"telco": "VIETTEL",
"value": 100000,
"fees": "18.5",
"penalty": "50"
}
]Biaya grup ditetapkan saat kartu diterima. Untuk nominal salah (status=2), penalti 50% dikenakan pada penerimaan setelah biaya grup: diterima = nominal yang dikonfirmasi penyedia × (100 − biaya grup)% × 50%, dibulatkan ke bawah ke satuan dong. Contoh 10.000đ, biaya grup 26,8%: diterima 3.660đ. Gunakan amount hasil final untuk mengkreditkan dana; jangan menghitung ulang dari nominal yang dinyatakan. Pesanan manual dapat tetap berstatus 99 hingga operator mengonfirmasi; pencarian/callback menjadi dasar pembaruan final.
4. Parameter dan respons top up
POST /api/rechargews
| command | Parameter khusus | Hasil |
|---|---|---|
| productlist | partner_id, sign | Katalog dan harga sesuai akun |
| getbalance | partner_id, sign | Saldo dompet VND |
| topup | request_id, service_code, amount, qty=1, account_info | Menerima pesanan dan mengembalikan order_code; belum berarti top up berhasil |
| getstatus | request_id dan order_code pesanan lama | Cari pesanan milik Merchant yang tepat |
| feetopup | partner_id, sign | Daftar key, value, price, discount sesuai akun |
| canceltopup | request_id, order_code, sign | Hanya membatalkan pesanan otomatis yang belum dialihkan untuk diproses; pesanan yang sudah dikirim atau hasilnya belum jelas akan ditolak |
| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| partner_id, command, sign | Semua perintah | Tanda tangan sesuai tabel autentikasi |
| request_id | topup, getstatus | Kode permintaan Anda, maksimal 100 karakter huruf/angka dan _ . : - untuk katalog yang dikonfigurasi |
| service_code | topup | Ambil persis dari productlist; jangan gunakan nama tampilan |
| amount | topup | Nominal atau jumlah top up sebelum diskon |
| qty | topup | Selalu 1 |
| account_info | topup | Object dengan key yang tepat sesuai options produk; nilai kolom berupa string |
| order_code | getstatus | Kode yang dikembalikan sistem saat membuat pesanan; jangan diganti dengan kode Nappay atau request_id |
1. Ambil katalog dan informasi pelanggan yang diperlukan
{
"partner_id": "YOUR_PARTNER_ID",
"command": "productlist",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_PRODUCTLIST"
}Baca status=success dan data berupa daftar produk. Setiap produk memiliki service_code, name, options, dan items. options menjelaskan key, name, type; jenis select memiliki choices. Untuk password/otp, simpan di server dan kirim hanya melalui HTTPS.
{
"status": "success",
"data": [
{
"service_code": "DEMO_GAME",
"name": "Sản phẩm minh họa",
"options": [
{
"key": "game_id",
"name": "ID nhân vật",
"type": "text"
}
],
"amount_mode": "fixed",
"amount_limits": null,
"items": [
{
"value": 100000,
"name": "Gói 100.000đ",
"price": "92500",
"discount": 7.5
}
]
}
]
}price adalah harga pembayaran, value adalah nominal. discount menunjukkan diskon akun, dihitung (value − price) / value × 100. Jangan menambahkan margin atau mengurangi diskon lagi pada price.
2. Paket tetap atau nominal yang dimasukkan pengguna
amount_mode=fixed: pilih value dari items. amount_mode=amount: baca amount_limits.minimum, maximum, dan multiple; kirim amount dalam batas yang diizinkan dan habis dibagi multiple. items dapat berisi paket teknis value=1, price=1; itu bukan paket penjualan 1đ. Gunakan amount_mode dan amount_limits untuk membangun formulir. Ambil ulang katalog jika konfigurasi/harga berubah; harga dikunci saat pesanan diterima.
3. Kirim pesanan
{
"partner_id": "YOUR_PARTNER_ID",
"command": "topup",
"request_id": "TOPUP_DEMO_001",
"service_code": "DEMO_GAME",
"amount": 100000,
"qty": 1,
"account_info": {
"game_id": "PLAYER_ID"
},
"sign": "MD5_PARTNER_KEY_PARTNER_ID_TOPUP_REQUEST_ID"
}JSON mengirim account_info sebagai object. Jika memakai form-urlencoded, kirim account_info sebagai string JSON atau account_info[game_id]=PLAYER_ID; jangan mengirim dua format yang bertentangan. Game dalam contoh hanya ilustrasi: ambil kolom sebenarnya dari productlist, jangan menganggap semua game memakai game_id.
{
"status": "success",
"data": {
"time": "2026-10-18 10:00:00",
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"pay_amount": 92500,
"discount": 7.5,
"service_code": "DEMO_GAME",
"amount": 100000,
"status": "pending"
}
}4. Pencarian dan saldo
{
"partner_id": "YOUR_PARTNER_ID",
"command": "getstatus",
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_GETSTATUS_REQUEST_ID"
}data.status: pending = sedang diproses; failed = gagal. Jika berhasil, respons pembuatan pesanan katalog internal dapat memakai success, sedangkan getstatus dan callback memakai completed. status=success di lapisan luar hanya berarti API menerima/menemukan permintaan. Periksa data.status untuk menetapkan hasil pesanan. getstatus mengembalikan request_value sebagai nominal; amount pesanan dengan penawaran internal adalah jumlah pembayaran.
{
"status": "success",
"data": {
"account": {
"game_id": "PLAYER_ID"
},
"request_value": 100000,
"amount": 92500,
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"status": "completed"
}
}{
"partner_id": "YOUR_PARTNER_ID",
"command": "getbalance",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_GETBALANCE"
}{
"status": "success",
"data": {
"balance": 1000000,
"currency": "VND"
}
}4. Parameter dan respons pembelian kode voucher
POST /api/cardws
| Perintah / alamat | Parameter khusus | Hasil |
|---|---|---|
| GET /api/cardws/products | partner_id | Array produk; cardvalue berisi value dan price |
| checkavailable | service_code, value, qty | stock_available: true/false |
| getbalance | sign; wallet_number jika dikirim harus sesuai Merchant | balance dan currency_code |
| buycard | request_id, service_code, value, qty, sign | Kode pesanan dan kartu, atau status menunggu |
| redownload | request_id, sign; order_code opsional | Ambil kembali hasil pesanan sebelumnya |
Katalog dan stok
curl "https://toctoico.com/api/cardws/products?partner_id=YOUR_PARTNER_ID"[
{
"name": "Thẻ minh họa",
"service_code": "DEMO_CARD",
"cardvalue": [
{
"service_code": "DEMO_CARD",
"value": 100000,
"id": "DEMO_SKU",
"price": 97000
}
]
}
]Pemeriksaan stok hanya merupakan snapshot pada saat request, bukan reservasi; pesanan pembelian tetap harus diproses dan saldonya diperiksa.
{
"partner_id": "YOUR_PARTNER_ID",
"command": "checkavailable",
"service_code": "DEMO_CARD",
"value": 100000,
"qty": 1
}{
"stock_available": true,
"message": "Còn hàng"
}Beli dan unduh ulang
{
"partner_id": "YOUR_PARTNER_ID",
"command": "buycard",
"request_id": "CARD_DEMO_001",
"wallet_number": "YOUR_WALLET",
"service_code": "DEMO_CARD",
"value": 100000,
"qty": 1,
"sign": "MD5_PARTNER_KEY_PARTNER_ID_BUYCARD_REQUEST_ID"
}qty antara 1 hingga 100, bergantung stok dan saldo. value adalah nominal, bukan harga setelah diskon. service_code dan harga diambil dari katalog sesuai Merchant.
{
"status": 1,
"message": "Mua thẻ thành công",
"data": {
"request_id": "CARD_DEMO_001",
"order_code": "S_DEMO_ORDER",
"cards": [
{
"serial": "DEMO_SERIAL",
"code": "DEMO_PIN",
"expired": "2027-12-31"
}
]
}
}| status | Arti |
|---|---|
| 1 | Selesai; terima cards dalam data |
| 2 | Pesanan penyedia sedang diproses; simpan request_id dan order_code untuk pencarian |
| 3 | Pesanan penyedia dibatalkan / direfund |
| Kode lain atau error | Error permintaan; baca message dan HTTP |
{
"partner_id": "YOUR_PARTNER_ID",
"command": "redownload",
"request_id": "CARD_DEMO_001",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_REDOWNLOAD_REQUEST_ID"
}Saat menunggu atau koneksi terputus, prioritaskan redownload memakai kode pesanan yang sudah diterima. Jangan mengubah request_id lalu membeli ulang. Beberapa jalur pemrosesan mengembalikan error kode duplikat saat buycard dipanggil ulang, sehingga redownload adalah langkah rekonsiliasi utama. Simpan kode/PIN dengan aman di server Anda.
{
"partner_id": "YOUR_PARTNER_ID",
"command": "getbalance",
"wallet_number": "YOUR_WALLET",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_GETBALANCE"
}{
"balance": 1000000,
"currency_code": "VND"
}5. Callback dan rekonsiliasi
Daftarkan URL callback Anda pada Merchant di web ini. Sistem mengirim hasil penukaran kartu/top up lewat POST JSON atau GET sesuai konfigurasi Merchant. Callback dapat dikirim ulang; tangani duplikat dengan aman berdasarkan kode pesanan dan status.
| Layanan | Periksa callback_sign | Kolom utama |
|---|---|---|
| Tukar kartu | MD5(partner_key + code + serial) | status, message, request_id, trans_id, declared_value, value, amount, telco, code, serial |
| Topup | MD5(partner_key + order_code) | status, request_id, order_code, request_value, amount, account |
Callback top up memakai status=completed untuk pesanan berhasil; request_value adalah nominal top up yang diminta dan amount adalah jumlah dibayar jika penawaran final tersedia. Pencarian pesanan dapat mengembalikan data.status=success untuk hasil yang sama. Callback GET penukaran kartu juga menyertakan card_value.
{
"status": "completed",
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"request_value": 100000,
"amount": 92500,
"account": {
"game_id": "PLAYER_ID"
},
"callback_sign": "MD5_PARTNER_KEY_ORDER_CODE"
}- Verifikasi tanda tangan dengan secret Merchant yang sesuai dan bandingkan secara aman; pastikan kode pesanan milik sistem Anda.
- Panggil check/getstatus untuk mengonfirmasi status serta mencocokkan nominal/jumlah uang sebelum mencatat hasil. Tanda tangan kompatibel tidak mencakup seluruh kolom status.
- Catat dalam transaksi database, cegah pencatatan/pengkreditan ganda, lalu segera kembalikan HTTP 2xx.
- Jika callback terlambat atau hilang, periksa secara berkala; jangan menganggap koneksi terputus sebagai kegagalan dan jangan membuat pesanan baru.
Antrean callback mencoba ulang maksimal 12 kali dengan jeda bertambah. Callback top up juga bergantung pada pengaturan callback produk. Hasil pembelian kode voucher diterima melalui buycard/redownload.
import hashlib, hmac
expected = hashlib.md5((partner_key + payload["order_code"]).encode()).hexdigest()
valid = hmac.compare_digest(expected, str(payload.get("callback_sign", "")).lower())6. Kode error dan pemecahan masalah
Baca kode HTTP, status, message, dan status transaksi dalam data. Jangan memakai satu tabel status untuk semua layanan: misalnya top up memakai string, sedangkan kode voucher memakai angka.
| Kode error umum | Penyebab |
|---|---|
| 100 | Produk tidak ditemukan atau permintaan penukaran kartu error; baca message |
| 101 | Dompet elektronik tidak ditemukan |
| 102 | Saldo dompet tidak cukup |
| 103 | Data tidak lengkap |
| 104 | Data yang dikirim tidak benar |
| 108 | Merchant tidak ditemukan, nonaktif, atau tidak valid |
| 109 | request_id sudah ada |
| 110 | Permintaan tidak valid, jenis Merchant salah, atau pesanan tidak ditemukan |
| 111 | wallet_number tidak cocok di API kode voucher |
| 116 | Tanda tangan salah |
| 117 | command tidak ada/salah |
| 118 | Stok produk habis atau layanan belum tersedia |
| 114 | IP tidak diizinkan |
| 129 | Pesanan kode voucher tidak ditemukan |
| 130 | Error penyedia |
| 125 | Jumlah tidak valid |
| 126 | Jumlah harus berupa bilangan bulat positif |
| Situasi | Penanganan |
|---|---|
| HTTP 400 | Periksa data, kolom pelanggan, batas nominal, dan pilihan produk |
| HTTP 409 / request_id duplikat | Periksa pesanan lama; jangan kirim payload berbeda dengan kode yang sudah dipakai |
| HTTP 503 / timeout | Pertahankan kode permintaan dan rekonsiliasi sebelum mencoba lagi; status pengiriman ke penyedia mungkin belum jelas |
| API success tetapi masih pending | Terus pantau status/callback; transaksi belum selesai |
| Sai sign | Periksa secret layanan yang benar, urutan penggabungan, request_id, dan UTF-8 |
| Harga berbeda dari tampilan sebelumnya | Ambil ulang katalog sesuai Merchant; harga ditetapkan saat pesanan diterima |
Berikan jarak minimal beberapa detik antar pemeriksaan dan tambah jeda jika hasil belum tersedia. request_id harus dibuat dan disimpan oleh server Anda sebelum dikirim. Jangan menggunakan pembelian nyata untuk memeriksa koneksi; productlist/getbalance/getfee merupakan pemeriksaan hanya baca.
7. Contoh kode
Contoh Python memanggil getbalance dengan aman untuk pemeriksaan koneksi (instal requests dengan pip install requests). Gunakan variabel lingkungan untuk data privat; jangan menulis secret dalam source code.
import os, hashlib, requests
BASE = "https://toctoico.com"
partner_id = os.environ["GACHTHE_PARTNER_ID"]
partner_key = os.environ["GACHTHE_PARTNER_KEY"]
def recharge(command, **fields):
request_id = str(fields.get("request_id", "")) if command in ("topup", "getstatus") else ""
sign = hashlib.md5((partner_key + partner_id + command + request_id).encode()).hexdigest()
payload = {**fields, "partner_id": partner_id, "command": command, "sign": sign}
response = requests.post(BASE + "/api/rechargews", json=payload, timeout=20)
result = response.json()
response.raise_for_status()
return result
print(recharge("getbalance"))
# Lưu request_id trước khi gọi topup. Sau đó lưu order_code trả về.
# Khi timeout: tra cứu/đối soát, không tạo request_id mới để mua lại.PHP: tanda tangan dan pengiriman JSON
<?php
$partnerId = getenv('GACHTHE_PARTNER_ID');
$partnerKey = getenv('GACHTHE_PARTNER_KEY');
$command = 'getbalance';
$payload = ['partner_id' => $partnerId, 'command' => $command,
'sign' => md5($partnerKey . $partnerId . $command)];
$ch = curl_init('https://toctoico.com/api/rechargews');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_TIMEOUT => 20]);
$body = curl_exec($ch);
if ($body === false) { throw new RuntimeException('Chưa xác nhận được phản hồi API'); }
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
// Kiểm tra $status, $result['status'] và trạng thái đơn trong data nếu có.cURL
curl --request POST 'https://toctoico.com/api/rechargews' \
--header 'Content-Type: application/json' \
--data '{"partner_id":"YOUR_PARTNER_ID","command":"getbalance","sign":"YOUR_COMPUTED_MD5"}' Penukaran kartu memakai rumus charging/check khusus pada tabel tanda tangan; rumus lama MD5(partner_key + code + serial) tetap didukung. Untuk membeli kode voucher, gunakan aturan penggabungan pada tabel tanda tangan dan endpoint /api/cardws.
8. Pencarian serial
POST /api/checkcard. Hanya tersedia jika admin mengaktifkan fitur dan memberikan izin kepada Merchant; kemampuan pencarian, batas, serta informasi yang dikembalikan bergantung pada konfigurasi yang disetujui. Jangan menyimpulkan kartu belum digunakan hanya dari respons yang tidak lengkap.
| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| partner_id | Wajib | Merchant dengan izin |
| telco | Khi charging | Kode jenis kartu |
| serial | Wajib | String serial |
| sign | Wajib | MD5(partner_key + serial) |
{
"partner_id": "YOUR_PARTNER_ID",
"telco": "VIETTEL",
"serial": "YOUR_CARD_SERIAL",
"sign": "MD5_PARTNER_KEY_SERIAL"
}Periksa status, message, dan data yang benar-benar dikembalikan. Jangan menganggapnya API top up voucher atau pemeriksaan PIN; jangan memberikan izin sendiri atau melakukan pencarian massal.
Tool Rejoin · API untuk reseller
1. Alur integrasi
API ini menjual, memperpanjang, dan mereset key VIP Tool Rejoin menggunakan dompet VND di TOCTOICO. Ini adalah protokol REST khusus Rejoin; jangan kirim command atau tanda tangan MD5 milik tiga layanan kartu/top up ke endpoint ini.
- Login web → Koneksi API, buat Merchant bertipe Tool Rejoin, aktifkan dan simpan Partner ID/Partner Key di backend Anda.
- Panggil GET /plans untuk memperoleh paket yang dijual, harga, dan service_ready. Baca GET /balance untuk memeriksa saldo dompet.
- Simpan kode pesanan Anda, plan_id, dan quantity sebelum mengirim POST /keys/purchase. Gunakan kode tersebut sebagai Idempotency-Key.
- Baca key dari respons berhasil atau periksa kembali GET /orders/{request_id} jika respons hilang. Tampilkan key dengan aman kepada pembeli.
- Perpanjang atau reset menggunakan UUID key milik akun Merchant; tiap tindakan memakai Idempotency-Key terpisah.
Masa berlaku key VIP dimulai saat aktivasi valid pertama di perangkat. Harga dibaca dari database ketika transaksi baru diproses, bukan dari harga yang Anda kirim. Harga dan jumlah hari dalam contoh hanya ilustrasi; gunakan GET /plans untuk membaca paket sebenarnya. Key Free diberikan melalui Discord, tidak dibeli atau direset melalui API reseller ini.
2. Autentikasi, izin, dan format
Authorization: Bearer YOUR_PARTNER_KEY
X-Partner-ID: YOUR_PARTNER_ID
Accept: application/json
Content-Type: application/json
Idempotency-Key: shop_order_20261030_000001| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| Authorization | Semua request | Bearer + Partner Key Merchant di web ini |
| X-Partner-ID | Semua request | Partner ID yang sesuai; jangan kirim kredensial di URL |
| Content-Type | Semua POST | application/json; reset tetap mengirim JSON object {} |
| Idempotency-Key | POST pembelian, perpanjangan, reset | 8–100 karakter A–Z, a–z, 0–9, _ . : -; kode tetap untuk satu tindakan |
| Accept | Direkomendasikan | application/json |
Merchant harus aktif, bertipe Tool Rejoin atau All, dan pemilik akun tidak dikunci. Jika IP diatur, request harus berasal dari IP publik server yang diizinkan; jangan menambahkan X-Forwarded-For atau header IP untuk melewati batas. Partner Key hanya disimpan di backend, bukan di JavaScript frontend, tautan, atau log.
Key dibatasi berdasarkan pemilik dompet, termasuk jika akun tersebut memiliki izin admin web; pengelolaan hanya untuk key VIP milik pemilik dompet yang dibeli lewat web atau API. Daftar pesanan API hanya milik Merchant yang memanggil. Idempotency-Key berlaku per Merchant dan dipakai bersama oleh semua jenis tindakannya; jangan memakai ulang kode pembelian untuk perpanjangan/reset. Dua Merchant berbeda memiliki ruang kode permintaan terpisah meskipun pemilik dompetnya sama.
Respons memakai success tipe boolean, data adalah hasil; error memiliki success=false, message, code dan data=null. Jangan membaca status memakai tabel angka API kartu. Nilai uang berupa bilangan bulat VND; waktu dikembalikan dalam ISO 8601 UTC. Halaman admin dapat menampilkannya dalam UTC+7.
{
"success": true,
"data": {
"balance": 500000,
"currency": "VND",
"discount_percent": 0,
"discount_bps": 0
}
}3. Daftar endpoint
| Method / path | Tujuan | Input |
|---|---|---|
| GET /api/rejoin/v1/balance | Saldo dompet bersama akun Merchant | Tanpa body |
| GET /api/rejoin/v1/plans | Paket yang dijual, harga saat ini, dan service_ready | Tanpa body |
| GET /api/rejoin/v1/keys | Daftar key VIP milik pemilik dompet, key disamarkan | page, per_page, status, q |
| GET /api/rejoin/v1/keys/{uuid} | Detail key VIP milik pemilik dompet, termasuk key lengkap | UUID dari license_ids atau keys.id |
| POST /api/rejoin/v1/keys/purchase | Potong saldo dan berikan key dalam transaksi yang sama | JSON: plan_id, quantity |
| POST /api/rejoin/v1/keys/{uuid}/renew | Potong saldo dan perpanjang key yang ada | JSON: plan_id |
| POST /api/rejoin/v1/keys/{uuid}/reset | Reset ikatan perangkat key VIP | JSON: {} |
| GET /api/rejoin/v1/orders | Riwayat tindakan Merchant saat ini, key disamarkan | page, per_page, request_id, operation |
| GET /api/rejoin/v1/orders/{request_id} | Periksa tindakan yang tepat sesuai Idempotency-Key yang dikirim | Kode permintaan asli, termasuk key lengkap jika tersedia |
| GET /api/rejoin/v1/users/{user_id}/discount | Khusus admin: lihat diskon pembelian key akun | ID akun web, bukan Partner ID |
| POST /api/rejoin/v1/users/{user_id}/discount | Khusus admin: tetapkan diskon pembelian key Rejoin | JSON: discount_percent; tidak memotong saldo atau memberikan key |
4. Saldo dan daftar paket
GET /balance mengembalikan balance, currency=VND, discount_percent, dan discount_bps milik pemilik akun Merchant. Ini dompet yang sama dengan transaksi web; jangan memotong dompet lain untuk pesanan yang sama tanpa rancangan rekonsiliasi.
{
"success": true,
"data": {
"balance": 500000,
"currency": "VND",
"discount_percent": 0,
"discount_bps": 0
}
}GET /plans mengembalikan data.items, currency, service_ready, dan diskon pembelian key akun tersebut. Dalam setiap paket: base_price adalah harga normal, price adalah harga satuan pembelian baru setelah diskon, renew_price adalah harga perpanjangan yang sudah menerapkan diskon Rejoin akun yang sama. discount_bps adalah persentase dikali 100 (2%=200 bps). Hanya kirim plan_id paket yang sedang dijual. service_ready=false berarti layanan VIP belum siap; ini belum memastikan key dapat dijual.
{
"success": true,
"data": {
"items": [
{
"id": 1,
"name": "VIP 7 ngày",
"days": 7,
"base_price": 50000,
"price": 50000,
"renew_price": 50000,
"discount_percent": 0,
"discount_bps": 0,
"reset_limit": 1,
"featured": 1
}
],
"currency": "VND",
"service_ready": true,
"discount_percent": 0,
"discount_bps": 0
}
}Diskon berlaku untuk pembelian dan perpanjangan key VIP Tool Rejoin, baik melalui web maupun API milik pemilik akun yang sama. Tidak berlaku untuk penukaran kartu, pembelian kode voucher, atau top up. Contoh harga normal 50.000đ, diskon 2%: price=49.000đ; pembelian 3 key memotong 147.000đ. Harga satuan dibulatkan HALF_UP ke satuan dong sebelum dikalikan jumlah (pecahan mulai 0,5đ dibulatkan ke atas). Jangan mengurangi diskon lagi dari price. Harga dan diskon ditetapkan saat pembelian atau perpanjangan; perubahan berikutnya tidak mengubah pesanan lama.
Admin: atur diskon per akun
Gunakan Merchant Tool Rejoin atau All dari akun admin aktif dimiliki. API membaca ulang izin admin dari database dalam transaksi; reseller biasa tidak boleh melihat atau mengedit endpoint ini, termasuk untuk dirinya sendiri. Jangan kirim role, owner_id, atau izin admin dalam body.
| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| user_id | Di alamat | ID bilangan bulat akun web yang akan diatur |
| discount_percent | POST wajib | Nilai 0–100, maksimal 2 angka desimal dengan titik; 0 untuk menghapus diskon |
{
"discount_percent": 2
}{
"success": true,
"data": {
"user_id": 123,
"discount_percent": 2,
"discount_bps": 200
}
}GET dan POST di alamat yang sama /api/rejoin/v1/users/123/discount. POST hanya menerima discount_percent, menetapkan nilai absolut, dan mencatat log admin; tidak mengakumulasi, mengubah pesanan lama, membuat pesanan di /orders, atau memerlukan Idempotency-Key. Mengirim ulang nilai yang sama mempertahankan tingkat diskon yang sama; jika admin kemudian mengubahnya, POST baru tetap menetapkan nilai yang dikirim. Error: 403 permission_denied jika bukan admin, 404 not_found jika akun tidak ditemukan, 400 invalid_request jika nilai atau kolom salah. Aturan autentikasi, Merchant, IP, dan batas request tetap berlaku.
curl --silent --show-error --include --request POST 'https://toctoico.com/api/rejoin/v1/users/123/discount' \
--header 'Authorization: Bearer YOUR_ADMIN_PARTNER_KEY' \
--header 'X-Partner-ID: YOUR_ADMIN_PARTNER_ID' \
--header 'Content-Type: application/json' \
--data '{"discount_percent":2}' 5. Beli key VIP
POST /api/rejoin/v1/keys/purchase
| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| plan_id | Wajib | ID bilangan bulat paket yang dijual dari GET /plans |
| quantity | Opsional, bawaan 1 | Bilangan bulat 1 hingga 50; total pembayaran = harga satuan pembelian baru setelah diskon × jumlah |
{
"plan_id": 1,
"quantity": 1
}Jangan mengirim price, discount_percent, discount_bps, charged, balance, duration_days, atau hak VIP untuk menentukan harga/hak sendiri. Jika berhasil, pemotongan saldo dan pemberian key dilakukan dalam transaksi yang sama; jika gagal, tidak ada pemberian key/pemotongan saldo parsial. Simpan request_id, order_code, license_ids, dan charged untuk rekonsiliasi.
{
"success": true,
"data": {
"request_id": "shop_order_20261030_000001",
"operation": "purchase",
"status": "completed",
"order_code": "RJ_EXAMPLE_ORDER",
"license_ids": [
"11111111-2222-4333-8444-555555555555"
],
"charged": 50000,
"balance": 450000,
"keys": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO0001-DEMO0002-DEMO0003-DEMO0004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
],
"created_at": "2026-10-30T02:00:00+00:00",
"pricing": {
"base_unit_price": 50000,
"unit_price": 50000,
"quantity": 1,
"discount_percent": 0,
"discount_bps": 0
}
}
}| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| request_id | Diterima | Idempotency-Key yang sudah Anda kirim |
| operation, status | Diterima | purchase dan completed ketika tindakan selesai |
| order_code | Diterima | Kode transaksi dompet sistem; dipakai untuk pencocokan admin |
| license_ids | Diterima | Daftar UUID key, bukan string key RJ-... |
| charged, balance | Diterima | Total dana dipotong dan saldo setelah tindakan, bilangan bulat VND |
| pricing | Diterima | Harga dan diskon final pembelian/perpanjangan; null untuk reset atau pesanan lama tanpa bukti snapshot |
| keys | Diterima | Key lengkap dan hak yang diberikan; simpan dengan aman, jangan catat di log |
| expires_at | Trong key | null jika belum diaktifkan; masa berlaku dimulai sejak aktivasi valid pertama |
6. Daftar dan detail key
GET /api/rejoin/v1/keys?page=1&per_page=20. page dimulai dari 1; gunakan per_page antara 1–100. status dapat berupa inactive, active, expired, atau revoked; q untuk pencarian. Paginasi mengembalikan page, per_page, total, dan pages. Daftar menyamarkan key; jangan masukkan key yang disamarkan ke tool.
{
"success": true,
"data": {
"items": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO••••••••00004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
],
"page": 1,
"per_page": 20,
"total": 1,
"pages": 1
}
}Panggil GET /api/rejoin/v1/keys/11111111-2222-4333-8444-555555555555 untuk menerima data berupa detail dengan key lengkap. Hanya mengembalikan key VIP pemilik akun; UUID salah atau key di luar hak akses ditolak. status=inactive belum diaktifkan; active masih berlaku; expired kedaluwarsa; revoked dicabut.
{
"success": true,
"data": {
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO0001-DEMO0002-DEMO0003-DEMO0004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
}| Parameter | Permintaan | Arti / nilai |
|---|---|---|
| id, owner_id, plan_id | Trong key | UUID key, ID pemilik akun, dan ID paket |
| kind, max_tabs | Trong key | vip, maksimal 4 tab |
| key | Trong key | Disamarkan dalam daftar; lengkap pada detail/pembelian/pencarian pesanan tertentu |
| device, device_id | Trong key | Nama perangkat dan hash kode perangkat; string kosong jika belum terikat atau sudah direset |
| generation | Trong key | Versi ikatan; meningkat saat reset untuk membatalkan konteks sesi lama |
| activated_at, expires_at, last_seen | Trong key | ISO 8601 UTC atau null; aktivasi pertama, kedaluwarsa, dan autentikasi terakhir |
| duration_days, resets_remaining | Trong key | Jumlah hari yang diberikan dan jatah reset tersisa |
| q | Filter daftar key | Maksimal 100 karakter; cari UUID, key tersamar, atau nama perangkat, bukan key lengkap |
7. Perpanjangan dan reset pindah perangkat
POST /api/rejoin/v1/keys/{uuid}/renew dengan body {"plan_id":1}, Idempotency-Key baru. Perpanjangan memakai renew_price yang sudah menerapkan diskon Rejoin pemilik akun saat diperpanjang. Jangan potong diskon lagi. Kolom pricing menyimpan harga dan persentase final pesanan perpanjangan. Key yang sudah diaktifkan: masa berlaku baru = max(masa berlaku lama, waktu sekarang) + jumlah hari paket. Key yang belum diaktifkan: tambahkan hari yang menunggu, masa berlaku belum dimulai. Tambahkan jatah reset sesuai paket; key dan ikatan perangkat tetap sama.
POST /api/rejoin/v1/keys/{uuid}/reset dengan body {}, Idempotency-Key baru. Reset memakai satu jatah, menghapus ikatan perangkat di server, menaikkan generation, dan mempertahankan masa berlaku. Reset mengembalikan charged=0, order_code=null; jika jatah habis, respons berupa error. Force reset tidak tersedia bagi reseller, key Free, atau key yang sudah dicabut.
Reset tidak menghapus file di ponsel, tidak memutus sesi seketika lewat push, dan tidak menambah masa berlaku otomatis. Tutup tool di perangkat lama sebelum reset agar perangkat lama tidak mengikat key kembali; buka tool di perangkat baru lalu masukkan key. Pencabutan sesi lama bergantung pada interval autentikasi tool.
{
"success": true,
"data": {
"request_id": "shop_reset_20261030_000001",
"operation": "reset",
"status": "completed",
"order_code": null,
"license_ids": [
"11111111-2222-4333-8444-555555555555"
],
"charged": 0,
"balance": 450000,
"keys": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO0001-DEMO0002-DEMO0003-DEMO0004",
"status": "active",
"device": "",
"expires_at": "2026-11-06T02:00:00+00:00",
"duration_days": 7,
"resets_remaining": 0,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": "2026-10-30T02:00:00+00:00",
"last_seen": "2026-10-30T02:00:00+00:00",
"device_id": "",
"generation": 2
}
],
"created_at": "2026-10-30T02:00:00+00:00",
"pricing": null
}
}Tool melakukan autentikasi ulang setiap 120 detik, dengan masa berlaku lease maksimal 300 detik. Ini bukan jaminan perangkat lama langsung terputus atau boleh dipakai offline 300 detik setelah setiap error jaringan; hentikan sesi lama sebelum pindah perangkat.
8. Pencarian pesanan dan retry yang aman
GET /api/rejoin/v1/orders/{request_id} gunakan Idempotency-Key yang tepat dari pembelian/perpanjangan/reset; jangan ganti dengan order_code atau string key di path ini. data dikembalikan dengan struktur yang sama seperti tindakan berhasil, termasuk key lengkap dalam hak akses. Jika kode belum dicatat Merchant ini, API mengembalikan 404.
GET /api/rejoin/v1/orders?page=1&per_page=20&operation=purchase mengembalikan data.items, page, per_page, total, pages. operation menerima purchase, renew, reset; request_id dapat memfilter kode tertentu. Key dalam daftar pesanan disamarkan. API Rejoin memproses transaksi langsung tanpa callback kartu/top up.
- Simpan kode permintaan + payload sebelum memanggil API, termasuk dari antrean.
- Timeout atau koneksi terputus: jangan menyimpulkan gagal, melakukan refund, atau mengirim pesanan baru dengan ID lain. Periksa GET /orders/{request_id}.
- Jika pesanan completed ditemukan, gunakan hasilnya; jangan membeli lagi. Jika 404, kirim ulang POST yang sama dengan Idempotency-Key dan payload yang sama.
- Retry dengan kode/isi yang sama mengembalikan hasil tersimpan tanpa pemotongan tambahan atau reset lagi. Kode sama dengan payload atau jenis tindakan berbeda mengembalikan 409.
- 429/503: tunggu sesuai Retry-After jika tersedia; periksa pesanan lama dan coba lagi secara terbatas dengan kode/isi yang sama. Jika status belum jelas, biarkan pesanan menunggu rekonsiliasi.
Jangan memakai timeout browser atau HTTP error sebagai bukti saldo belum dipotong. Perubahan harga paket atau diskon akun setelah pesanan selesai tidak mengubah charged dan pricing final pesanan tersebut.
Hasil pencarian/retry mempertahankan request_id, order_code, charged, pricing, license_ids, dan saldo setelah tindakan tersebut. pricing berisi base_unit_price (harga normal), unit_price (harga satuan setelah diskon), quantity, discount_percent, dan discount_bps dari snapshot pesanan, bukan diskon saat ini. Pesanan lama tanpa snapshot mengembalikan pricing=null, bukan menebak diskon. Kolom keys mencerminkan status key saat ini: mungkin sudah diperpanjang, direset, atau diikat ke perangkat setelah transaksi lama. Untuk saldo saat ini, panggil GET /balance. Retry reset setelah key diikat ke perangkat baru tetap mengembalikan penyelesaian tindakan lama, tanpa reset lagi.
{
"success": true,
"data": {
"items": [
{
"request_id": "shop_order_20261030_000001",
"operation": "purchase",
"status": "completed",
"order_code": "RJ_EXAMPLE_ORDER",
"license_ids": [
"11111111-2222-4333-8444-555555555555"
],
"charged": 50000,
"balance": 450000,
"keys": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO••••••••00004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
],
"created_at": "2026-10-30T02:00:00+00:00",
"pricing": {
"base_unit_price": 50000,
"unit_price": 50000,
"quantity": 1,
"discount_percent": 0,
"discount_bps": 0
}
}
],
"page": 1,
"per_page": 20,
"total": 1,
"pages": 1
}
}9. HTTP dan error
| HTTP | Arti / penanganan |
|---|---|
| 400 | JSON, parameter, atau Idempotency-Key tidak ada/salah; perbaiki data sebelum membuat tindakan baru |
| 401 | Partner ID atau Bearer Partner Key tidak ada/salah, atau Merchant nonaktif |
| 402 | Saldo dompet tidak cukup; isi saldo dan periksa kembali |
| 403 | Jenis Merchant salah, IP tidak diizinkan, akun dikunci, atau tindakan tidak diberi izin |
| 404 | Paket/key/pesanan tidak tersedia dalam hak akses; baca code dan message untuk membedakan |
| 405 | HTTP method salah; gunakan GET/POST sesuai tabel endpoint |
| 409 | Konflik Idempotency-Key atau jatah reset habis; jangan mengganti kode untuk melewati error |
| 429 | Batas request terlampaui; kurangi kecepatan, tunggu, lalu periksa pesanan lama |
| 503 | VIP atau layanan belum siap; simpan kode permintaan untuk rekonsiliasi |
| kode umum | HTTP |
|---|---|
| authentication_required, authentication_failed, merchant_disabled | 401 |
| module_forbidden, account_denied, ip_forbidden, permission_denied, purchase_limit | 403 |
| not_found | 404 |
| method_not_allowed | 405 |
| invalid_request | 400 |
| idempotency_conflict, reset_limit | 409 |
| insufficient_balance | 402 |
| rate_limited | 429 |
| vip_unavailable, service_unavailable, discount_unavailable | 503 |
{
"success": false,
"data": null,
"code": "insufficient_balance",
"message": "Số dư không đủ. Vui lòng nạp thêm tiền."
}Batas saat ini: 100 request/menit per Merchant dan 120 request/menit per IP di API Rejoin; setiap request dihitung dalam keduanya. HTTP 429 mengharuskan menunggu minimal hingga jendela menit berikutnya. Baca HTTP beserta success/code/message. Jangan menerapkan kode error numerik chargingws/cardws pada Rejoin. Jangan catat Authorization, Partner Key, atau key VIP lengkap di log bantuan.
Batas pembelian akun/grup tetap berlaku untuk API: nilai per transaksi, total pembelian harian, dan batas untuk akun yang belum mengaktifkan autentikasi dua faktor. Jika terlampaui, respons HTTP 403, code=purchase_limit. Ini bukan permintaan memasukkan kode 2FA ke API; periksa konfigurasi akun/batas sebelum mencoba lagi. Tindakan yang tidak diizinkan, seperti mereset key yang dicabut, dapat mengembalikan permission_denied (403).
Berhasil mengembalikan HTTP 200. Respons berisi key lengkap dikirim dengan Cache-Control: no-store; jangan cache publik atau menyimpan key mentah di localStorage/log. Simpan data pemberian key di backend dan tampilkan hanya kepada pembeli yang sudah diautentikasi.
10. Contoh kode lengkap
Jalankan dari backend reseller dengan verifikasi HTTPS bawaan aktif. Isi variabel lingkungan memakai Merchant di web ini. Jangan memakai client API challenge/activate tool sebagai pengganti API penjualan. Contoh POST pembelian/perpanjangan akan memotong saldo jika memakai informasi nyata; reset memakai jatah.
cURL: katalog, pembelian, dan pencarian setelah timeout
export REJOIN_PARTNER_ID='YOUR_PARTNER_ID'
export REJOIN_PARTNER_KEY='YOUR_PARTNER_KEY'
API='https://toctoico.com/api/rejoin/v1'
REQUEST_ID='shop_order_20261030_000001' # Persist before submitting.
curl --silent --show-error --include "$API/plans" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include --request POST "$API/keys/purchase" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $REQUEST_ID" \
--data '{"plan_id":1,"quantity":1}'
# On timeout, look up the original request ID. Never create a replacement ID.
curl --silent --show-error --include "$API/orders/$REQUEST_ID" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"cURL: lihat key, riwayat, perpanjangan, dan reset
Gunakan kembali variabel API/Partner dari contoh di atas. Ganti KEY_ID dengan UUID yang diterima dalam license_ids; setiap POST berikut memakai kode tindakan baru yang disimpan sebelum request dikirim.
KEY_ID='UUID_FROM_LICENSE_IDS'
curl --silent --show-error --include "$API/balance" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include "$API/keys?page=1&per_page=20&status=inactive" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include "$API/keys/$KEY_ID" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include "$API/orders?page=1&per_page=20&operation=purchase" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
# Renewal debits the wallet using the selected plan price.
curl --silent --show-error --include --request POST "$API/keys/$KEY_ID/renew" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: shop_renew_20261030_000001" \
--data '{"plan_id":1}'
# Stop the tool on the old device first. Reset consumes one allowance.
curl --silent --show-error --include --request POST "$API/keys/$KEY_ID/reset" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: shop_reset_20261030_000001" \
--data '{}' Python 3: beli, periksa pesanan, dan retry dengan kode yang sama
Memakai pustaka standar; tidak perlu menginstal requests. Variabel REJOIN_ORDER_ID harus disimpan bersama pesanan Anda sebelum dikirim.
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
BASE = os.environ.get("REJOIN_API_BASE", "https://toctoico.com/api/rejoin/v1").rstrip("/")
PARTNER_ID = os.environ["REJOIN_PARTNER_ID"]
PARTNER_KEY = os.environ["REJOIN_PARTNER_KEY"]
class APIError(Exception):
def __init__(self, status, result):
if not isinstance(result, dict):
result = {"message": "HTTP error without a valid API object"}
super().__init__(result.get("message", "API request failed"))
self.status, self.result = status, result
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None # Never forward API credentials to a redirected host.
OPENER = urllib.request.build_opener(NoRedirect())
def api(method, path, payload=None, request_id=None):
headers = {"Authorization": "Bearer " + PARTNER_KEY,
"X-Partner-ID": PARTNER_ID, "Accept": "application/json"}
if request_id is not None:
headers["Idempotency-Key"] = request_id
data = None
if payload is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(payload).encode("utf-8")
request = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
try:
with OPENER.open(request, timeout=20) as response:
try:
result = json.load(response)
except (ValueError, UnicodeError):
raise urllib.error.URLError("Invalid API response; outcome not confirmed") from None
except urllib.error.HTTPError as error:
try:
result = json.load(error)
except (ValueError, UnicodeError):
result = {"message": "HTTP error without a valid JSON body"}
raise APIError(error.code, result) from None
if not isinstance(result, dict):
raise urllib.error.URLError("Invalid API response; outcome not confirmed")
if result.get("success") is not True:
raise APIError(200, result)
if not isinstance(result.get("data"), dict):
raise urllib.error.URLError("Invalid API response; outcome not confirmed")
return result["data"]
def order(request_id):
return api("GET", "/orders/" + urllib.parse.quote(request_id, safe=""))
def purchase(plan_id, quantity, request_id):
# Save request_id + payload in your database BEFORE calling this function.
payload = {"plan_id": plan_id, "quantity": quantity}
try:
return api("POST", "/keys/purchase", payload, request_id)
except APIError as error:
if error.status not in (429, 503):
raise # Never replace a rejected 409 payload with a new request ID.
time.sleep(60 if error.status == 429 else 2)
except (urllib.error.URLError, TimeoutError):
pass
# A lost response does not mean the purchase failed. Look up the SAME ID.
try:
return order(request_id)
except APIError as error:
if error.status != 404:
raise
# Only one bounded retry, with exactly the same payload and request ID.
# If this also times out, persist "needs reconciliation" and poll order().
return api("POST", "/keys/purchase", payload, request_id)
if __name__ == "__main__":
# Use a plan_id returned by GET /plans, never infer it from a display name.
result = purchase(int(os.environ["REJOIN_PLAN_ID"]), 1, os.environ["REJOIN_ORDER_ID"])
# Store/show result["keys"] only through a protected customer flow, not logs.
print(json.dumps({name: result[name] for name in
("request_id", "order_code", "charged", "license_ids")}, ensure_ascii=False))PHP 8 + cURL: kontrak yang sama dan kode retry yang sama
<?php
$base = rtrim(getenv('REJOIN_API_BASE') ?: 'https://toctoico.com/api/rejoin/v1', '/');
$partnerId = getenv('REJOIN_PARTNER_ID');
$partnerKey = getenv('REJOIN_PARTNER_KEY');
class RejoinApiError extends RuntimeException {
public int $httpStatus;
public function __construct(int $status, array $result) {
parent::__construct($result['message'] ?? 'API request failed');
$this->httpStatus = $status;
}
}
class RejoinTransportError extends RuntimeException {}
function rejoinApi(string $method, string $path, ?array $payload = null,
?string $requestId = null): array {
global $base, $partnerId, $partnerKey;
$headers = ['Authorization: Bearer ' . $partnerKey,
'X-Partner-ID: ' . $partnerId, 'Accept: application/json'];
if ($requestId !== null) $headers[] = 'Idempotency-Key: ' . $requestId;
$ch = curl_init($base . $path);
$options = [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20,
CURLOPT_FOLLOWLOCATION => false];
if ($payload !== null) {
$headers[] = 'Content-Type: application/json';
// Empty reset payload must be a JSON object {}, never an array [].
$options[CURLOPT_POSTFIELDS] = json_encode($payload === [] ? new stdClass() : $payload,
JSON_THROW_ON_ERROR);
}
$options[CURLOPT_HTTPHEADER] = $headers;
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false) throw new RejoinTransportError('Response not confirmed');
try {
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
if ($status < 200 || $status >= 300) {
throw new RejoinApiError($status, ['message' => 'HTTP error without valid JSON']);
}
throw new RejoinTransportError('Invalid JSON; outcome not confirmed');
}
if (!is_array($result)) {
if ($status < 200 || $status >= 300) {
throw new RejoinApiError($status, ['message' => 'HTTP error without a valid API object']);
}
throw new RejoinTransportError('Invalid API object; outcome not confirmed');
}
if ($status < 200 || $status >= 300 || ($result['success'] ?? false) !== true) {
throw new RejoinApiError($status, $result);
}
if (!is_array($result['data'] ?? null)) {
throw new RejoinTransportError('Invalid API data; outcome not confirmed');
}
return $result['data'];
}
function rejoinOrder(string $requestId): array {
return rejoinApi('GET', '/orders/' . rawurlencode($requestId));
}
function rejoinPurchase(int $planId, int $quantity, string $requestId): array {
// Persist the request ID and payload in your database before sending.
$payload = ['plan_id' => $planId, 'quantity' => $quantity];
try {
return rejoinApi('POST', '/keys/purchase', $payload, $requestId);
} catch (RejoinApiError $error) {
if (!in_array($error->httpStatus, [429, 503], true)) throw $error;
sleep($error->httpStatus === 429 ? 60 : 2);
} catch (RejoinTransportError $error) {
// Continue with order lookup; never generate a replacement ID here.
}
try {
return rejoinOrder($requestId);
} catch (RejoinApiError $error) {
if ($error->httpStatus !== 404) throw $error;
}
// One retry with the identical payload and request ID.
// If still uncertain, keep the order for reconciliation using rejoinOrder().
return rejoinApi('POST', '/keys/purchase', $payload, $requestId);
}
$requestId = getenv('REJOIN_ORDER_ID'); // Stable business order ID, saved before I/O.
$planId = (int) getenv('REJOIN_PLAN_ID'); // Read from GET /plans.
$result = rejoinPurchase($planId, 1, $requestId);
// Reset example: stop the old tool first, then persist a NEW reset request ID.
// $uuid = $result['license_ids'][0];
// $reset = rejoinApi('POST', '/keys/' . $uuid . '/reset', [], 'shop_reset_20261030_000001');
// Store/show $result['keys'] only through a protected customer flow, not logs.
$summary = array_intersect_key($result, array_flip(['request_id', 'order_code', 'charged', 'license_ids']));
echo json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);