Game Top-up API

Sell game top-ups in your own store. List products, check a player ID, place an order, get the result by webhook.

Basics

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.

Typical flow

  1. GET /api/v1/products and show products to your customer.
  2. POST /api/v1/validate to show the player's in-game name before they pay.
  3. Collect payment from your customer.
  4. POST /api/v1/order. The result is processing, then success or failed (failed orders are refunded to your wallet automatically).
  5. Receive the final status by webhook, or poll GET /api/v1/order/{order_id}.

Wallet

GET/api/v1/balance
{ "success": true, "data": { "balance": 4881.2, "currency": "INR" } }

Games and products

GET/api/v1/games
{ "success": true, "data": [ { "code": "mlbb", "name": "Mobile Legends", "products": 6 } ] }
GET/api/v1/products?game=mlbb

game 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).

Validate a player ID

POST/api/v1/validate

Send 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

Place an order

POST/api/v1/order

order_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).

Order status

GET/api/v1/order/{order_id}
GET/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.

Webhooks

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));
}

Errors

HTTPcodeMeaning
400INVALID_USER_ID, INVALID_ZONE_ID, INVALID_ORDER_ID…A field is missing or malformed
401UNAUTHORIZEDMissing or wrong API key
402INSUFFICIENT_BALANCETop up your wallet
403CLIENT_DISABLED, IP_NOT_ALLOWEDAccount disabled or IP not on your allow-list
404PRODUCT_NOT_FOUND, ORDER_NOT_FOUNDUnknown id
409DUPLICATE_ORDER, PRODUCT_UNAVAILABLE, PRICE_UNAVAILABLEAlready exists / cannot be sold right now
422INVALID_PLAYERPlayer ID does not exist
429RATE_LIMITEDSlow down
503VALIDATOR_UNAVAILABLEID check temporarily down, retry

Example (curl)

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"}'