/api/v1/balance
Test this endpoint first to verify the key, signature, IP allowlist, and rate limit.
{
"username": "gmp_1234abcd",
"ref_id": "balance-20260802-0001",
"sign": "YOUR_MD5_RESULT"
}
For resellers & integrators
Connect your bot, website, or sales system directly to your Gempay balance. This guide covers the complete sandbox-to-production flow, the MLBB Region API, safe timeout handling, and callback verification.
Complete integration guide
Follow this guide from key creation to go-live. All examples use JSON, prices are integer Indonesian rupiah amounts, and sandbox and production use the same response contract. The same key can also access the MLBB Region API.
rc 03/rc 99, query status using the same
ref_id. Never create a new ref_id because the first transaction may already be processing.
Transport rules
POST.Content-Type: application/json.data.rc.Required authentication fields
sign = md5(username + secret + ref_id)
// Do not add colons, pipes, spaces, or newlines between values.
// Example hash source:
gmp_1234abcdYOUR_SECRETorder-20260802-0001
Keep credentials in server environment variables. The helper below works with every endpoint; add endpoint-specific fields through the final argument.
<?php
function gempayRequest(string $endpoint, string $refId, array $payload = []): array
{
$baseUrl = getenv('GEMPAY_API_BASE_URL');
$username = getenv('GEMPAY_API_USERNAME');
$secret = getenv('GEMPAY_API_SECRET');
$body = array_merge($payload, [
'username' => $username,
'ref_id' => $refId,
'sign' => md5($username.$secret.$refId),
]);
$curl = curl_init($baseUrl.'/'.$endpoint);
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($curl);
if ($raw === false) throw new RuntimeException(curl_error($curl));
return json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
}
$result = gempayRequest('balance', 'balance-20260802-0001');
import { createHash } from 'node:crypto';
async function gempayRequest(endpoint, refId, payload = {}) {
const baseUrl = process.env.GEMPAY_API_BASE_URL;
const username = process.env.GEMPAY_API_USERNAME;
const secret = process.env.GEMPAY_API_SECRET;
const sign = createHash('md5')
.update(username + secret + refId, 'utf8')
.digest('hex');
const response = await fetch(`${baseUrl}/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...payload, username, ref_id: refId, sign }),
signal: AbortSignal.timeout(20_000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
curl --request POST 'https://www.alantashop.id/api/v1/balance' \
--header 'Content-Type: application/json' \
--data '{
"username": "gmp_1234abcd",
"ref_id": "balance-20260802-0001",
"sign": "YOUR_MD5_RESULT"
}'
Generate the signature in your system. Never paste secrets into shell history on a shared server.
| Endpoint | Purpose | Additional fields |
|---|---|---|
| POST /price-list | Lists products and prices for the key owner's tier. | None. |
| POST /transaction | Creates a transaction or returns an idempotent replay. | buyer_sku_code, tujuan. |
| POST /status | Returns the latest status for a ref_id. | None. |
| POST /balance | Returns the current reseller balance. | None. |
| POST /ml-region | Checks an MLBB nickname, region, and service type. | user_id, zone_id; production requires an active plan. |
Test this endpoint first to verify the key, signature, IP allowlist, and rate limit.
{
"username": "gmp_1234abcd",
"ref_id": "balance-20260802-0001",
"sign": "YOUR_MD5_RESULT"
}
The price already reflects the key owner's pricing tier. Refresh the catalog regularly. Sell only when both product status fields are true; check stock when unlimited_stock is false, and obey cut-off times. Only use the public buyer_sku_code.
{
"username": "gmp_1234abcd",
"ref_id": "pricelist-20260802-0001",
"sign": "YOUR_MD5_RESULT"
}
{
"data": {
"price_list": [{
"buyer_sku_code": "ml-86",
"product_name": "Mobile Legends 86 Diamonds",
"input_fields": [
{"key":"user_id","type":"numeric","required":true,"min":5,"max":20,"options":[]},
{"key":"zone_id","type":"numeric","required":true,"min":1,"max":10,"options":[]}
],
"price": 21000,
"buyer_product_status": true,
"seller_product_status": true,
"unlimited_stock": true,
"stock": 0,
"multi": true,
"start_cut_off": null,
"end_cut_off": null
}],
"rc": "00",
"message": "Transaksi sukses"
}
}
Creates an order from the member balance. The server recalculates the price; do not send a price field.
{
"username": "gmp_1234abcd",
"ref_id": "order-20260802-0001",
"sign": "YOUR_MD5_RESULT",
"buyer_sku_code": "ml-86",
"tujuan": {"user_id": "123456789", "zone_id": "1234"}
}
Use the original transaction ref_id and a signature generated from that same value. Only the key that created a transaction can read it. A missing transaction returns rc 01.
Uses the same H2H authentication. Sandbox returns deterministic results without a plan. Production requires an active Region API plan plus both the key allowlist and plan IP whitelist.
{
"username": "gmp_1234abcd",
"ref_id": "ml-check-20260804-0001",
"sign": "YOUR_MD5_RESULT",
"user_id": "106101371",
"zone_id": "2540"
}
Never guess or concatenate identifiers yourself. For each SKU, read input_fields from the price list and send a tujuan object with matching keys. The server validates and constructs the supplier destination.
"tujuan": {
"customer_no": "081234567890"
}"tujuan": {
"user_id": "123456789",
"zone_id": "1234"
}"tujuan": {
"user_id": "123456789",
"server": "os_asia"
}| Genshin/HSR server code | Region |
|---|---|
| os_asia | Asia |
| os_usa | America |
| os_euro | Europe |
| os_cht | TW, HK, MO |
A select field lists valid values in options. Send numeric fields as strings to preserve leading zeroes. Never alter destination digits.
API payloads use a data envelope. Store at least ref_id, rc, status, price, and sn, plus a redacted raw response for reconciliation.
| rc | Meaning | Required action |
|---|---|---|
| 00 | Success | Mark successful and store the SN when present. |
| 01 | Failed / status not found | Final for a transaction; create a new order only on explicit user intent. |
| 02 | Supplier rejected | Final; retry with a new ref_id only after fixing the cause. |
| 03 | Pending | Poll status using the same ref_id. Do not reorder. |
| 40 | Missing parameter / invalid ref_id | Fix the payload and resend. |
| 41 | Authentication failed | Check username, secret, signature source, key, and account status. |
| 42 | IP not allowed | Add the correct public server IP or use the correct key. |
| 43 | Rate limit exceeded | Wait for the next minute and use queue backoff. |
| 51 | Insufficient balance | Fund the balance; the response may include balance and required. |
| 52 | SKU not found | Refresh the price list and check buyer_sku_code. |
| 53 | Product unavailable / cut-off | Disable it temporarily and refresh the catalog. |
| 54 | Invalid destination | Validate against input_fields; never alter digits automatically. |
| 55 | Repeat transaction unsupported | Do not repeat the same product and destination that day. |
| 56 | Amount outside limits | Correct the amount according to product rules. |
| 57 | Region API plan inactive | Activate or renew the plan on the production key. |
| 58 | Region API quota exhausted | Purchase a new monthly period or wait for the next period. |
| 99 | Uncertain due to internal error | Poll status using the same ref_id. Do not reorder. |
Public status values remain Sukses, Pending, or Gagal. Use rc as the primary decision field.
/status with the original ref_id.Idempotency is scoped per key. Replaying a transaction ref_id returns the first transaction without another order or debit. Poll pending transactions every 5 seconds initially, then slow to 10-30 seconds.
Sandbox does not debit balance, create sales orders, or contact suppliers. Validation, response envelopes, idempotency, and callbacks follow the production contract.
| ref_id ending | Result | Example |
|---|---|---|
| Odd digit | rc 00 / Sukses | test-order-1 |
| Digit 0 | rc 03 / Pending | test-order-0 |
| Even digit except 0, or non-digit | rc 01 / Gagal | test-order-2, test-order-x |
A key's mode cannot change. After all branches pass, create a new production key and run a small smoke test.
When a callback HTTPS URL is configured, Gempay sends POST application/json. Callbacks do not use the data envelope. Return HTTP 2xx quickly after verifying and storing the payload.
X-Gempay-Signature: HMAC_SHA256_HEX
Content-Type: application/json
{
"ref_id": "order-20260802-0001",
"status": "Sukses",
"buyer_sku_code": "ml-86",
"customer_no": "1234567891234",
"price": 21000,
"balance": 479000,
"sn": "SN-123456789",
"rc": "00",
"message": "Transaksi sukses"
}
$rawBody = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_GEMPAY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $rawBody, getenv('GEMPAY_API_SECRET'));
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
// Upsert by ref_id, then return 200.
Never put a secret in mobile apps, browser JavaScript, logs, screenshots, chat, or repositories.
Separate bots, websites, staging, and production so each key can be revoked independently.
Use stable public egress IPs. The allowlist matches exact IPv4/IPv6 addresses, not CIDR ranges.
Create a new key, move traffic, verify success, then revoke the old key.
Never log signatures, secrets, raw callbacks, voucher SNs, or complete destination values.
Reject invalid signatures and make handlers idempotent because callbacks may be repeated.
| Symptom | What to check |
|---|---|
| Always rc 41 | Check the prefix, active key/account, exact ref_id in the hash and body, username+secret+ref_id order, and lowercase hexadecimal MD5 output. |
| rc 42 in production | Find the server's public egress IP. Private/container addresses are often different. |
| rc 43 too soon | The limit is per key per calendar minute, not per IP. Use a global queue across all workers sharing the key. |
| rc 54 for destination | Match keys, types, min/max, required, and options from the latest input_fields. Send numbers as strings. |
| Transaction timeout | Do not create a new ref_id. Replay transaction or query status with the same key and ref_id. |
| Callback 401 | Use the raw request body, matching key secret, SHA-256 hexadecimal output, and timing-safe comparison. |
| Callback keeps retrying | Return HTTP 2xx after storing a valid payload. Redirects are not followed and non-2xx is failure. |
| Price differs from public web | This can be correct: price-list uses the key owner's pricing tier. Treat API price as reseller cost. |
| Stale status after callback failure | Call the status endpoint. Callback failure does not change or reprocess a transaction. |
This browser-only checklist is not saved. Use it before moving real traffic.