API SHOMERCH memakai REST, menerima dan membalas JSON. Base URL:
https://shomerch.web.id/api/v1
Empat hal yang perlu disiapkan sebelum request pertama:
Cek dulu apakah semuanya siap:
curl 'https://shomerch.web.id/api/v1/ping' \ -H 'X-API-Key: str-live-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
Kirim API key di header X-API-Key pada setiap request.
Bisa juga sebagai Authorization: Bearer str-live-….
QRIS statis tidak mengunci nominal, dan Shopee 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 100–199 pada setiap transaksi. Pesanan Rp10.000 menjadi Rp10.181, 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 (Rp100–199) tetap masuk ke akun ShopeePay Anda.
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
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 | integer | Wajib. Harga produk dalam rupiah. Minimal 1000. |
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 (10181) 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']; // 10181 $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'])
201{
"success": true,
"data": {
"id": "trx_9f1c8a2e-...",
"user_id": "usr_3a7e1b4c-...",
"ref_id": "INV-2026-0001",
"amount": 10000,
"unique_code": 181,
"total_amount": 10181, // ← 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"
}
}
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.
GET/api/v1/transactions
| Query | Keterangan |
|---|---|
status | Filter status |
from, to | Rentang tanggal, format YYYY-MM-DD |
page | Halaman, mulai 1 |
limit | Baris per halaman, maks 100 (default 20) |
POST/api/v1/transactions/:ref_id/cancel
Hanya berlaku untuk transaksi yang masih PENDING.
Membatalkan juga melepas nominalnya supaya bisa dipakai pesanan lain.
GET/api/v1/transactions/:ref_id/qr.png?size=512
Mengembalikan PNG. 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">
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.
GET/api/v1/mutations
Riwayat transaksi mentah yang terbaca dari akun ShopeePay Anda — termasuk pembayaran yang tidak berasal dari SHOMERCH. Berguna untuk rekonsiliasi pembukuan.
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" }
}
}
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>
{
"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",
"amount": 10000,
"unique_code": 181,
"total_amount": 10181,
"status": "PAID",
"paid_at": "2026-09-08T14:05:47+07:00",
"payment_method": "QRIS_SHOPEEPAY",
"metadata": { "product_id": 42 }
}
}
| Event | Kapan dikirim |
|---|---|
payment.paid | Pembayaran terdeteksi dan cocok |
payment.expired | QR kedaluwarsa tanpa pembayaran |
payment.canceled | Dibatalkan lewat API atau dashboard |
payment.failed | Pembayaran gagal di sisi Shopee |
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.
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:
stringify ulang —
urutan key bisa berubah dan tanda tangan langsung tidak cocok.===.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
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.
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.
| 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 | 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.
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 |
|---|---|---|
| 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 | Kredensial ShopeePay belum diisi atau tidak valid. |
| 403 | MERCHANT_SESSION_EXPIRED | Sesi Shopee mati. Ambil ulang token & cookie dari DevTools. |
| 403 | ACCOUNT_SUSPENDED | Akun dibekukan. Hubungi dukungan. |
| 409 | UNIQUE_CODE_EXHAUSTED | 100 slot kode unik untuk nominal itu sedang terpakai semua. Coba lagi beberapa menit. |
| 422 | VALIDATION_ERROR | Field tidak valid. 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. |
| 502 | SHOPEE_UNREACHABLE | Shopee sedang tidak bisa dihubungi. Coba lagi. |
| 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.
total_amount, bukan amount.
Itu yang dibayar pelanggan dan yang muncul di mutasi ShopeePay Anda.X-Shomerch-Event-Id untuk membuang duplikat — retry bisa membuat
event yang sama datang dua kali.ref_id yang 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.MERCHANT_SESSION_EXPIRED di aplikasi Anda.
Tampilkan pesan ke admin toko, jangan biarkan pelanggan melihat error mentah.