Sell game top-ups in your own store. List products, check a player ID, place an order, get the result by webhook.
All requests are JSON over HTTPS. Send your API key in the x-api-key header. Keep it on your server, never in a web page or app.
x-api-key: th_xxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json
Every response has success. On success the payload is in data. On failure you get code and message.
{ "success": true, "data": { ... } }
{ "success": false, "code": "INSUFFICIENT_BALANCE", "message": "Insufficient wallet balance" }
Prices are in the currency shown in each response (currency). Orders are paid from your prepaid wallet.
GET /api/v1/products and show products to your customer.POST /api/v1/validate to show the player's in-game name before they pay.POST /api/v1/order. The result is processing, then success or failed (failed orders are refunded to your wallet automatically).GET /api/v1/order/{order_id}./api/v1/balance{ "success": true, "data": { "balance": 4881.2, "currency": "INR" } }
/api/v1/games{ "success": true, "data": [ { "code": "mlbb", "name": "Mobile Legends", "products": 6 } ] }
/api/v1/products?game=mlbbgame is optional. Only order products where available is true.
{
"success": true,
"data": [{
"id": 12,
"game_code": "mlbb",
"game_name": "Mobile Legends",
"name": "86 Diamonds",
"price": 118.8,
"currency": "INR",
"available": true,
"requires_user_id": true,
"requires_zone_id": true,
"requires_character_name": false,
"zone_mode": "manual",
"zone_options": [],
"can_validate": true
}]
}
zone_mode is none (no zone needed), manual (player types their zone / server ID) or select (pick one value from zone_options, e.g. os_asia for Genshin).
/api/v1/validateSend product_id (or game_code), user_id and, when required, zone_id. Limit: 30 per minute.
POST /api/v1/validate
{ "product_id": 12, "user_id": "12345678", "zone_id": "2569" }
200 { "success": true, "data": { "valid": true, "player_name": "PlayerName", "region": "IN" } }
422 { "success": false, "code": "INVALID_PLAYER", "message": "Invalid player ID" }
503 { "success": false, "code": "VALIDATOR_UNAVAILABLE", ... } // retry shortly
/api/v1/orderorder_id is your own unique id (max 64 chars). Sending the same order_id again never creates or charges a second order: you get 409 DUPLICATE_ORDER with the existing order, so it is safe to retry after a timeout.
POST /api/v1/order
{
"order_id": "ORDER-10001",
"product_id": 12,
"user_id": "12345678",
"zone_id": "2569",
"character_name": "", // only if requires_character_name
"webhook_url": "https://store.example.com/h" // optional, overrides your default webhook
}
200 {
"success": true,
"data": {
"order_id": "ORDER-10001",
"reference": "THMF3K9A1B2C3",
"status": "processing",
"product": "Mobile Legends - 86 Diamonds",
"user_id": "12345678",
"zone_id": "2569",
"player_name": "PlayerName",
"price": 118.8,
"currency": "INR",
"refunded": false,
"created_at": "2026-11-14T08:00:00.000Z",
"updated_at": "2026-11-14T08:00:00.000Z"
}
}
status: processing (not final), success (delivered, final), failed (final, wallet refunded).
/api/v1/order/{order_id}/api/v1/orders?limit=25&before={order_id}Same order object as above. Webhooks are the better way to learn the result; use polling as a backup.
When an order becomes success or failed we POST to your webhook URL. Set it with PUT /api/v1/webhook (body {"url":"https://..."}) or ask your account manager. Your signing secret is returned by GET /api/v1/webhook.
POST https://store.example.com/h
X-Webhook-Timestamp: 1768472100
X-Webhook-Signature: <hex HMAC-SHA256(secret, timestamp + "." + rawBody)>
{ "event": "order.updated", "data": { "order_id": "ORDER-10001", "status": "success", ... } }
Respond with any 2xx within 10 seconds. Failed deliveries are retried (30s, 2m, 10m, 1h, 6h, 12h). Always verify the signature against the raw body, reject timestamps older than 5 minutes, and treat duplicates safely.
// Node.js
const crypto = require("crypto");
function verify(rawBody, ts, sig, secret) {
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const exp = crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
return exp.length === sig.length && crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(sig));
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | INVALID_USER_ID, INVALID_ZONE_ID, INVALID_ORDER_ID… | A field is missing or malformed |
| 401 | UNAUTHORIZED | Missing or wrong API key |
| 402 | INSUFFICIENT_BALANCE | Top up your wallet |
| 403 | CLIENT_DISABLED, IP_NOT_ALLOWED | Account disabled or IP not on your allow-list |
| 404 | PRODUCT_NOT_FOUND, ORDER_NOT_FOUND | Unknown id |
| 409 | DUPLICATE_ORDER, PRODUCT_UNAVAILABLE, PRICE_UNAVAILABLE | Already exists / cannot be sold right now |
| 422 | INVALID_PLAYER | Player ID does not exist |
| 429 | RATE_LIMITED | Slow down |
| 503 | VALIDATOR_UNAVAILABLE | ID check temporarily down, retry |
curl -X POST https://YOUR-DOMAIN/api/v1/order \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"order_id":"ORDER-10001","product_id":12,"user_id":"12345678","zone_id":"2569"}'