Daftar isi
Referensi API · v1
Dokumentasi API
API SHOMERCH memakai REST, menerima dan membalas JSON. Satu API key, satu bentuk data, dan satu bentuk webhook — untuk semua penyedia QRIS.
https://shomerch.web.id/api/v1
Mulai cepat
Empat hal yang perlu disiapkan sebelum request pertama:
- Akun dengan paket aktif
- Payment gateway tersambung — ShopeePay, GoPay, BCA, DANA, Livin' Merchant, BNI, atau BRI (Dashboard → Payment Gateway). Opsional: Binance Pay (USDT) sebagai metode tambahan
- API key (Dashboard → API Keys)
- Endpoint webhook, kalau ingin diberi tahu otomatis
Cek dulu apakah semuanya siap:
curl 'https://shomerch.web.id/api/v1/ping' \ -H 'X-API-Key: str-live-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
Autentikasi
Kirim API key di header X-API-Key pada setiap request.
Bisa juga sebagai Authorization: Bearer str-live-….
Kode unik — kenapa Rp10.000 jadi Rp10.081
QRIS statis tidak mengunci nominal, dan portal merchant tidak menyediakan API "cek status invoice". Yang tersedia hanya daftar transaksi masuk. Jadi pencocokan dilakukan lewat nominal.
Supaya dua pesanan tidak tertukar, SHOMERCH menambahkan kode unik 1–99 pada setiap transaksi. Pesanan Rp10.000 menjadi Rp10.081, dan nominal itu dijamin tidak sama dengan pesanan lain yang sedang berjalan di merchant Anda.
total_amount, bukan amountamount adalah harga produk Anda. total_amount adalah yang harus
dibayar pelanggan dan yang terkunci di dalam QR. Kalau Anda menampilkan
amount, pelanggan akan bingung melihat nominal berbeda di aplikasi bank.
Selisih kode unik (Rp1–99) tetap masuk ke akun merchant Anda.
Pengecualian: GoPay tanpa kode unik
Untuk merchant GoPay, QR dibuat persis senilai harga (unique_code = 0,
total_amount = amount). Sebagai gantinya, satu nominal hanya boleh
dipakai satu pesanan aktif per merchant. Selama pesanan Rp10.000 masih menunggu
pembayaran, pesanan Rp10.000 berikutnya ditolak dengan 409 NOMINAL_IN_USE beserta
error.retry_after (detik) dan header Retry-After. Nominal yang berbeda
tetap bisa berjalan bersamaan.
Nominal dilepas begitu pesanan itu lunas. Kalau pesanan kedaluwarsa atau dibatalkan, nominal baru dilepas tiga menit setelah waktu kedaluwarsanya, supaya pembayaran telat dari pembeli sebelumnya tidak melunasi pesanan orang lain.
Binance Pay (USDT) — metode tambahan
Selain QRIS, transaksi bisa dibayar dalam USDT lewat Binance Pay ke akun Binance
pribadi Anda. Ini tidak menggantikan QRIS: transaksi tanpa
method tetap QRIS seperti biasa, dan Binance hanya dipakai untuk
transaksi yang dibuat dengan "method": "binance". Jadi bot Anda bisa
menampilkan dua tombol — QRIS dan Binance Pay — dan memanggil
endpoint yang sama dengan method yang berbeda.
- Sambungkan di Dashboard → Payment Gateway → Binance Pay: API Key + Secret Key (izin Enable Reading saja — key yang bisa trading/withdraw ditolak), Pay ID, dan (opsional) isi QR Binance Pay Anda. Lalu Tes Koneksi.
- Panggil
GET /me: tampilkan tombol Binance hanya kalaupayment_methodsmemuat"binance". - Buat transaksi dengan
"method": "binance"danamountdalam USDT (maks 2 desimal, minimal 0.1). - Tunjukkan ke pembeli:
total_amountUSDT,pay_to(Pay ID), dan QR-nya kalauqr_image_urltidaknull. - Pembeli mengirim USDT lewat Binance Pay. Lunas terdeteksi dalam ±10 detik, lalu webhook
payment.paiddikirim denganpayment_method: "BINANCE_PAY".
curl -X POST 'https://shomerch.web.id/api/v1/transactions' \ -H 'X-API-Key: str-live-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \ -H 'Content-Type: application/json' \ -d '{ "ref_id": "INV-2026-0002", "method": "binance", "amount": 5 }'
{
"success": true,
"data": {
"id": "trx_4b2d...",
"ref_id": "INV-2026-0002",
"method": "binance",
"currency": "USDT",
"amount": 5,
"unique_code": 0, // 0.01, 0.02, … kalau 5.00 sedang dipakai pesanan lain
"total_amount": 5, // ← yang harus dikirim pembeli, PERSIS
"pay_to": "123456789", // Binance Pay ID tujuan
"qr_string": "https://app.binance.com/...",
"qr_image_url": "https://shomerch.web.id/api/v1/transactions/INV-2026-0002/qr.png",
"status": "PENDING",
"expires_at": "2026-09-08T14:08:11+07:00"
}
}
Nominal: pas dulu, penanda sekecil mungkin
QR Binance Pay pribadi tidak bisa mengunci nominal — pembeli mengetik nominalnya sendiri
di aplikasi Binance. Karena itu pesanan pertama dibayar pas (5.00). Kalau ada pesanan
lain senominal yang masih menunggu, berikutnya mendapat 5.01, lalu 5.02, dan seterusnya — paling banyak
+0.20 USDT. Lebih dari 21 pesanan aktif senominal ditolak dengan
409 UNIQUE_CODE_EXHAUSTED. Seperti GoPay, nominal pesanan yang kedaluwarsa/dibatalkan baru
dilepas tiga menit setelah waktu kedaluwarsanya.
5.01), persis seperti yang diketik di aplikasi Binance.
Alur lengkap
1. Pelanggan checkout → POST /transactions 2. Tampilkan QR + total_amount → qr_string / qr_image_url 3. Pelanggan scan & bayar → (di aplikasi bank/e-wallet mereka) 4. SHOMERCH mendeteksi (~10 detik) → status jadi PAID 5. Webhook payment.paid dikirim → sistem Anda memproses pesanan Kalau tidak dibayar sampai expires_at + 3 menit: → status jadi EXPIRED, webhook payment.expired dikirim
Cek koneksi
GET/api/v1/ping
Satu-satunya endpoint yang tidak butuh paket aktif. Pakai ini saat memasang integrasi untuk memastikan key benar dan merchant sudah tersambung.
{
"success": true,
"data": {
"pong": true,
"api_key": { "name": "Bot Telegram", "mode": "live" },
"plan_active": true,
"merchant_status": "active", // atau not_connected / expired / invalid / unverified
"server_time": "2026-09-08T14:03:11+07:00"
}
}
Buat transaksi
POST/api/v1/transactions
| Field | Tipe | Keterangan |
|---|---|---|
ref_id | string | Wajib. Nomor pesanan Anda. Unik per akun — mengirim ulang ref_id yang sama mengembalikan transaksi lama, tidak membuat yang baru. |
amount | number | Wajib. QRIS: harga dalam rupiah, bilangan bulat, minimal 1000. Binance Pay: harga dalam USDT, maks 2 desimal, minimal 0.1. |
method | string | qris (bawaan, boleh tidak dikirim) atau binance. Lihat Binance Pay. |
expires_in | integer | Umur QR dalam detik. 60–1800, default 300. |
customer | object | { name, email } — opsional, hanya untuk catatan Anda. |
callback_url | string | Webhook khusus untuk transaksi ini, menimpa endpoint global. |
metadata | object | Data bebas maks 2 KB. Dikembalikan apa adanya di webhook. |
curl -X POST 'https://shomerch.web.id/api/v1/transactions' \ -H 'X-API-Key: str-live-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \ -H 'Content-Type: application/json' \ -d '{ "ref_id": "INV-2026-0001", "amount": 10000, "metadata": { "product_id": 42 } }'
const res = await fetch('https://shomerch.web.id/api/v1/transactions', { method: 'POST', headers: { 'X-API-Key': process.env.SHOMERCH_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ ref_id: 'INV-2026-0001', amount: 10000, metadata: { product_id: 42 }, }), }); const { success, data, error } = await res.json(); if (!success) throw new Error(error.message); // Tampilkan data.total_amount (10081) dan data.qr_string ke pelanggan. console.log(data.total_amount, data.qr_string);
$ch = curl_init('https://shomerch.web.id/api/v1/transactions'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-Key: ' . getenv('SHOMERCH_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'ref_id' => 'INV-2026-0001', 'amount' => 10000, ]), ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (empty($res['success'])) { throw new Exception($res['error']['message']); } $total = $res['data']['total_amount']; // 10081 $qr = $res['data']['qr_string'];
import os, requests r = requests.post( 'https://shomerch.web.id/api/v1/transactions', headers={'X-API-Key': os.environ['SHOMERCH_KEY']}, json={'ref_id': 'INV-2026-0001', 'amount': 10000}, timeout=15, ) body = r.json() if not body['success']: raise RuntimeError(body['error']['message']) data = body['data'] print(data['total_amount'], data['qr_string'])
Respons 201
{
"success": true,
"data": {
"id": "trx_9f1c8a2e-...",
"user_id": "usr_3a7e1b4c-...",
"ref_id": "INV-2026-0001",
"method": "qris",
"currency": "IDR",
"amount": 10000,
"unique_code": 81,
"total_amount": 10081, // ← yang dibayar pelanggan
"qr_string": "00020101021226610016ID.CO.SHOPEE.WWW...",
"qr_image_url": "https://shomerch.web.id/api/v1/transactions/INV-2026-0001/qr.png",
"status": "PENDING",
"created_at": "2026-09-08T14:03:11+07:00",
"expires_at": "2026-09-08T14:08:11+07:00"
}
}
Cek status transaksi
GET/api/v1/transactions/:ref_id
curl 'https://shomerch.web.id/api/v1/transactions/INV-2026-0001' \ -H 'X-API-Key: str-live-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
Status yang mungkin: PENDING,
PAID, EXPIRED,
CANCELED, FAILED.
Daftar transaksi
GET/api/v1/transactions
| Query | Keterangan |
|---|---|
status | Filter status. Salah satu dari PENDING, PAID, EXPIRED, FAILED, CANCELED. |
from, to | Rentang tanggal pembuatan, format YYYY-MM-DD |
updated_since | Waktu ISO-8601. Hanya transaksi yang berubah setelah waktu ini, diurutkan dari yang paling lama. Lihat bagian polling. |
page | Halaman, mulai 1 |
limit | Baris per halaman, maks 100 (default 20) |
Baris pada endpoint ini tidak menyertakan
qr_string dan
qr_image_url — ambil satu transaksi lewat
GET /transactions/:ref_id kalau Anda membutuhkannya.
{
"success": true,
"data": {
"transactions": [ /* … */ ],
"next_since": "2026-09-08T14:05:47+07:00", // penanda polling berikutnya
"pagination": { "page": 1, "limit": 20, "total": 37, "total_pages": 2 }
}
}
Batalkan transaksi
POST/api/v1/transactions/:ref_id/cancel
Hanya berlaku untuk transaksi yang masih PENDING.
Membatalkan juga melepas nominalnya supaya bisa dipakai pesanan lain.
Gambar QR
GET/api/v1/transactions/:ref_id/qr.png?size=512
Mengembalikan PNG. Parameter size adalah sisi gambar
dalam piksel, dijepit ke rentang 120–1024 (default 480).
Praktis untuk ditempel langsung di HTML atau dikirim lewat bot:
<img src="https://shomerch.web.id/api/v1/transactions/INV-1/qr.png?size=400">
Transaksi Binance Pay tanpa QR tersimpan tidak punya gambar:
qr_image_url-nya null dan endpoint ini membalas 404 QR_NOT_AVAILABLE —
tunjukkan pay_to saja.
Endpoint ini tetap butuh header X-API-Key, jadi untuk
ditampilkan di browser sebaiknya QR di-render sendiri dari qr_string
atau di-proxy lewat server Anda.
Mutasi uang masuk
GET/api/v1/mutations
Riwayat transaksi mentah yang terbaca dari akun merchant Anda — termasuk pembayaran yang tidak berasal dari SHOMERCH. Berguna untuk rekonsiliasi pembukuan.
| Query | Keterangan |
|---|---|
direction | IN (uang masuk) atau OUT. Kosong berarti keduanya. |
from, to | Rentang tanggal, format YYYY-MM-DD (waktu WIB) |
page | Halaman, mulai 1 |
limit | Baris per halaman, maks 100 (default 20) |
{
"success": true,
"data": {
"mutations": [{
"provider": "shopeepay",
"provider_transaction_id": "2609081405…",
"shopee_transaction_id": "2609081405…", // nama lama, nilainya sama
"direction": "IN",
"amount": 10081,
"currency": "IDR", // USDT untuk Binance Pay
"status": "Berhasil",
"store_name": "Toko Saya",
"occurred_at": "2026-09-08T14:05:47+07:00"
}],
"pagination": { "page": 1, "limit": 20, "total": 5, "total_pages": 1 }
}
}
Tiap baris memuat provider
(shopeepay, gopay,
bca, dana,
livin, bni, bri, atau binance) dan
provider_transaction_id. Field lama
shopee_transaction_id masih dikirim dengan nilai yang sama
supaya integrasi yang sudah jalan tidak perlu diubah.
Info akun
GET/api/v1/me
{
"success": true,
"data": {
"user_id": "usr_3a7e...",
"name": "Toko Saya",
"plan": { "code": "basic", "name": "Basic", "expires_at": "2026-10-08T..." },
"quota": { "limit": 50, "used": 38, "remaining": 12, "resets_at": "2026-09-09T00:00:00+07:00" },
"merchant": { "status": "active", "name": "Toko Saya" },
"payment_methods": [ // metode yang siap dipakai SEKARANG
{ "method": "qris", "provider": "shopeepay", "name": "ShopeePay", "currency": "IDR" },
{ "method": "binance", "provider": "binance", "name": "Binance Pay", "currency": "USDT" }
]
}
}
Pakai payment_methods untuk menentukan tombol pembayaran di bot Anda:
tombol Binance Pay cukup ditampilkan kalau "binance" ada di daftar ini.
Webhook
Setiap kali status transaksi berubah, kami mengirim POST
ke endpoint Anda dengan header berikut:
X-Shomerch-Event : payment.paid X-Shomerch-Event-Id : 6f1b2c8a-... # idempotency key X-Shomerch-Timestamp : 1788677476 # epoch detik X-Shomerch-Signature : sha256=<hex>
Payload
{
"event": "payment.paid",
"created_at": "2026-09-08T14:05:52+07:00",
"data": {
"id": "trx_9f1c...",
"user_id": "usr_3a7e...",
"ref_id": "INV-2026-0001",
"method": "qris", // atau "binance"
"currency": "IDR", // atau "USDT"
"amount": 10000,
"unique_code": 81,
"total_amount": 10081,
"status": "PAID",
"paid_at": "2026-09-08T14:05:47+07:00",
"payment_method": "QRIS_SHOPEEPAY", // BINANCE_PAY untuk Binance
"metadata": { "product_id": 42 }
}
}
Event
| Event | Kapan dikirim |
|---|---|
payment.paid | Pembayaran terdeteksi dan cocok |
payment.expired | QR kedaluwarsa tanpa pembayaran |
payment.canceled | Dibatalkan lewat API atau dashboard |
payment.failed | Dicadangkan. Status FAILED belum pernah dihasilkan oleh sistem pencocokan, jadi event ini belum pernah terkirim. Boleh Anda tangani sejak sekarang. |
Retry
Endpoint Anda harus membalas HTTP 2xx dalam 10 detik. Kalau tidak, kami ulang sampai 6 kali: langsung, 30 detik, 2 menit, 10 menit, 1 jam, 6 jam. Setelah itu ditandai gagal dan bisa dikirim ulang manual dari dashboard.
Proses pesanan Anda setelah membalas 200 — jangan tahan respons sampai pekerjaan berat selesai, atau kami akan menganggapnya timeout dan mengirim ulang.
Verifikasi tanda tangan
payment.paid palsu dan mendapat barang tanpa membayar.
Tanda tangan dihitung dari timestamp + "." + body mentah
memakai HMAC SHA-256 dengan signing secret Anda. Tiga hal yang harus benar:
- Pakai body mentah, bukan hasil parse lalu di-
stringifyulang — urutan key bisa berubah dan tanda tangan langsung tidak cocok. - Bandingkan dengan fungsi timing-safe, bukan
===. - Tolak kalau timestamp lebih tua dari 5 menit — ini yang mencegah serangan replay.
const crypto = require('crypto'); // PENTING: ambil body mentah, jangan express.json() untuk route ini. app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body.toString('utf8'); const timestamp = req.get('x-shomerch-timestamp'); const signature = (req.get('x-shomerch-signature') || '').replace('sha256=', ''); // 1. Tolak yang kedaluwarsa (anti replay) if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(400).send('timestamp kedaluwarsa'); } // 2. Hitung ulang tanda tangan const expected = crypto .createHmac('sha256', process.env.SHOMERCH_WEBHOOK_SECRET) .update(`${timestamp}.${raw}`) .digest('hex'); // 3. Bandingkan timing-safe const a = Buffer.from(signature), b = Buffer.from(expected); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).send('tanda tangan tidak valid'); } const event = JSON.parse(raw); // Balas dulu, proses belakangan — batas waktu kami 10 detik. res.sendStatus(200); if (event.event === 'payment.paid') { // Idempoten: event yang sama bisa datang dua kali. prosesPesanan(event.data.ref_id, event.data.total_amount); } });
<?php $raw = file_get_contents('php://input'); $timestamp = $_SERVER['HTTP_X_SHOMERCH_TIMESTAMP'] ?? ''; $signature = str_replace('sha256=', '', $_SERVER['HTTP_X_SHOMERCH_SIGNATURE'] ?? ''); // 1. Anti replay if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('timestamp kedaluwarsa'); } // 2. Hitung ulang $expected = hash_hmac('sha256', $timestamp . '.' . $raw, getenv('SHOMERCH_WEBHOOK_SECRET')); // 3. Bandingkan timing-safe if (!hash_equals($expected, $signature)) { http_response_code(401); exit('tanda tangan tidak valid'); } $event = json_decode($raw, true); http_response_code(200); // balas dulu if ($event['event'] === 'payment.paid') { prosesPesanan($event['data']['ref_id']); }
import hmac, hashlib, time, os from flask import Flask, request app = Flask(__name__) @app.route('/webhook', methods=['POST']) def webhook(): raw = request.get_data() # bytes mentah timestamp = request.headers.get('X-Shomerch-Timestamp', '') signature = request.headers.get('X-Shomerch-Signature', '').replace('sha256=', '') # 1. Anti replay if abs(time.time() - int(timestamp or 0)) > 300: return 'timestamp kedaluwarsa', 400 # 2. Hitung ulang expected = hmac.new( os.environ['SHOMERCH_WEBHOOK_SECRET'].encode(), timestamp.encode() + b'.' + raw, hashlib.sha256, ).hexdigest() # 3. Bandingkan timing-safe if not hmac.compare_digest(expected, signature): return 'tanda tangan tidak valid', 401 event = request.get_json() if event['event'] == 'payment.paid': proses_pesanan(event['data']['ref_id']) return '', 200
Tanpa webhook — cara polling yang benar
Webhook itu opsional. Kalau aplikasi Anda tidak punya alamat publik — bot Telegram atau WhatsApp yang jalan di laptop, di rumah, atau di belakang NAT — biarkan saja kolom webhook kosong. Tidak ada yang rusak; status transaksi tetap diperbarui, Anda tinggal menanyakannya.
/transactions/:ref_id untuk tiap pesanan yang menunggu adalah
cara paling boros. Sepuluh pesanan aktif yang dicek tiap 5 detik =
120 request/menit — jauh di atas jatah paket mana pun, dan Anda
akan kena RATE_LIMITED.
Cara yang benar: satu panggilan untuk semua pesanan
Pakai updated_since pada endpoint daftar. Satu
panggilan mengembalikan semua transaksi yang berubah sejak pengecekan
terakhir — berapa pun jumlah pesanan yang sedang berjalan.
| Query | Keterangan |
|---|---|
updated_since | Waktu ISO-8601. Hanya transaksi yang berubah setelah waktu ini yang dikembalikan, diurutkan dari yang paling lama. |
status | Saring, mis. PAID saja. |
Respons memuat next_since — pakai nilai itu apa
adanya untuk panggilan berikutnya. Kalau tidak ada yang berubah, nilainya dipantulkan
balik, jadi tidak ada yang terlewat.
// Satu loop untuk SELURUH pesanan. 12 request/menit, tetap segitu // meski ada 200 pesanan berjalan. let sejak = new Date().toISOString(); setInterval(async () => { const url = 'https://shomerch.web.id/api/v1/transactions' + '?status=PAID&updated_since=' + encodeURIComponent(sejak); const r = await fetch(url, { headers: { 'X-API-Key': process.env.SHOMERCH_KEY }, }); const { data } = await r.json(); for (const trx of data.transactions) { // Idempoten: simpan ref_id yang sudah diproses, event yang sama // bisa muncul lagi kalau penanda waktu mundur. if (await sudahDiproses(trx.ref_id)) continue; await kirimBarang(trx.ref_id, trx.total_amount); } sejak = data.next_since; // penanda untuk putaran berikutnya }, 5000);
import os, time, requests from datetime import datetime, timezone sejak = datetime.now(timezone.utc).isoformat() sudah = set() while True: r = requests.get( 'https://shomerch.web.id/api/v1/transactions', headers={'X-API-Key': os.environ['SHOMERCH_KEY']}, params={'status': 'PAID', 'updated_since': sejak}, timeout=15, ) data = r.json()['data'] for trx in data['transactions']: if trx['ref_id'] in sudah: continue sudah.add(trx['ref_id']) kirim_barang(trx['ref_id'], trx['total_amount']) sejak = data['next_since'] time.sleep(5)
curl 'https://shomerch.web.id/api/v1/transactions?status=PAID&updated_since=2026-09-08T14:00:00%2B07:00' \ -H 'X-API-Key: str-live-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' # Respons memuat next_since — pakai itu untuk panggilan berikutnya.
Berapa sering boleh poll?
| Paket | Rate limit | Interval aman | Sisa untuk request lain |
|---|---|---|---|
| Basic | 30/menit | 5 detik (12/menit) | 18/menit |
| Standard | 60/menit | 3 detik (20/menit) | 40/menit |
| Premium | 120/menit | 2 detik (30/menit) | 90/menit |
PENDING, tidak ada gunanya terus memanggil.
Mulai loop saat pesanan pertama dibuat, hentikan saat semuanya selesai atau
kedaluwarsa. Ini menghemat kuota dan membuat paket Basic cukup untuk kebanyakan bot.
Webhook atau polling?
| Webhook | Polling | |
|---|---|---|
| Butuh alamat publik + HTTPS | Ya | Tidak |
| Jeda deteksi | Seketika | Sesuai interval poll |
| Memakan rate limit | Tidak | Ya |
| Cocok untuk | Web/VPS | Bot di laptop, rumah, di balik NAT |
Boleh juga dipakai bersamaan: webhook sebagai jalur utama, polling jarang (mis. tiap 1 menit) sebagai jaring pengaman kalau ada webhook yang tidak sampai.
Kode error
Semua error memakai bentuk yang sama:
{
"success": false,
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Kuota harian paket Basic (50) sudah habis.",
"docs": "https://shomerch.web.id/docs#errors"
}
}
| HTTP | Code | Artinya & apa yang harus dilakukan |
|---|---|---|
| 400 | INVALID_JSON | Body request bukan JSON yang valid. Periksa Content-Type dan tanda kutipnya. |
| 401 | INVALID_API_KEY | Key salah atau sudah dicabut. Buat key baru di dashboard. |
| 402 | SUBSCRIPTION_INACTIVE | Paket belum aktif atau habis. Perpanjang di dashboard. |
| 403 | MERCHANT_NOT_CONNECTED | Payment gateway belum disambungkan, string QRIS statisnya kosong, atau kredensialnya tidak valid. Untuk "method": "binance": Binance Pay belum disambungkan atau belum lolos Tes Koneksi. |
| 403 | MERCHANT_SESSION_EXPIRED | Sesi penyedia mati. Ambil ulang kredensialnya dari DevTools portal merchant. |
| 403 | ACCOUNT_SUSPENDED | Akun dibekukan. Hubungi dukungan. |
| 404 | TRANSACTION_NOT_FOUND | ref_id itu tidak ada di akun Anda. Perhatikan huruf besar-kecilnya. |
| 404 | QR_NOT_AVAILABLE | Transaksi Binance Pay tanpa QR tersimpan. Tunjukkan pay_to (Pay ID) ke pembeli. |
| 404 | NOT_FOUND | Endpoint-nya yang tidak ada — biasanya salah ketik path atau versi. |
| 409 | TRANSACTION_NOT_PENDING | Hanya transaksi PENDING yang bisa dibatalkan. |
| 409 | UNIQUE_CODE_EXHAUSTED | 99 slot kode unik untuk nominal itu sedang terpakai semua (Binance Pay: 21 slot, mis. 5.00–5.20). Coba lagi beberapa menit. |
| 409 | NOMINAL_IN_USE | Khusus GoPay: nominal yang sama masih dipakai pesanan lain di merchant Anda. Coba lagi setelah error.retry_after detik (juga di header Retry-After), atau setelah pesanan itu lunas/dibatalkan. |
| 422 | VALIDATION_ERROR | Field tidak valid — termasuk method yang tidak dikenal atau nominal USDT lebih dari 2 desimal. Lihat error.details untuk per-field. |
| 429 | RATE_LIMITED | Melebihi request/menit. Tunggu sesuai header Retry-After. |
| 429 | QUOTA_EXCEEDED | Kuota harian habis. Reset jam 00:00 WIB, atau naikkan paket. |
| 500 | QRIS_BUILD_FAILED | String QRIS statis merchant tidak bisa diubah jadi QR dinamis. Periksa ulang QRIS statisnya di dashboard. |
| 500 | INTERNAL_ERROR | Kesalahan di sisi kami. Laporkan error.request_id yang ikut dikirim. |
Setiap respons endpoint yang kena rate limit membawa header berikut, jadi Anda tidak perlu menebak sisa jatah:
X-RateLimit-Limit : 30 # request per menit untuk paket Anda X-RateLimit-Remaining : 27 # sisa di jendela berjalan X-RateLimit-Reset : 1788677520# epoch detik saat jendela direset Retry-After : 12 # hanya saat 429
Batas & kuota
| Batas | Basic | Standard | Premium |
|---|---|---|---|
| Transaksi per hari | 50 | 100 | Tanpa batas |
| Request per menit | 30 | 60 | 120 |
| API key | 2 | 5 | 20 |
| Riwayat tersimpan | 30 hari | 90 hari | 365 hari |
Kuota harian dihitung dari transaksi yang dibuat (bukan yang lunas)
dan reset jam 00:00 WIB. Sisa kuota bisa dilihat kapan saja lewat
GET /api/v1/me.
Praktik terbaik
- Simpan
total_amount, bukanamount. Itu yang dibayar pelanggan dan yang muncul di mutasi merchant Anda. - Perlakukan webhook sebagai idempoten. Pakai
X-Shomerch-Event-Iduntuk membuang duplikat — retry bisa membuat event yang sama datang dua kali. - Pakai
ref_idyang benar-benar unik. Nomor pesanan Anda sendiri, bukan angka acak — supaya kalau request timeout, mengirim ulang akan mengembalikan transaksi yang sama, bukan membuat QR kedua. - Jangan bergantung pada polling saja. Webhook jauh lebih hemat kuota dan lebih cepat.
- Tangani
MERCHANT_SESSION_EXPIREDdi aplikasi Anda. Tampilkan pesan ke admin toko, jangan biarkan pelanggan melihat error mentah. - Beri toleransi waktu. QR default hidup 5 menit; deteksi butuh beberapa detik setelah pembayaran. Jangan tutup halaman pembayaran terlalu cepat.