Dokumentasi API
Gateway QRIS untuk membuat kode bayar, memverifikasi transaksi ShopeePay, dan mengirim webhook saat pembayaran lunas.
http://localhost:3001
Ringkasan
Gateway ini menjembatani toko Anda dengan ShopeePay Partner API. Ada tiga tugas utama: membuat QRIS dinamis dari nominal, memantau transaksi masuk, dan memberi tahu toko ketika pembayaran terverifikasi.
| Tahap | Aktor | Yang terjadi |
|---|---|---|
| 1 | Toko | Tentukan nominal dasar dan order_id, lalu panggil POST /create-qris. |
| 2 | Gateway | Memilih suffix unik 0–99, membentuk nominal final, dan mengembalikan string QRIS. |
| 3 | Pelanggan | Scan QRIS dan membayar nominal final. |
| 4 | Gateway | Poller memeriksa transaksi ShopeePay setiap POLL_INTERVAL_SECONDS. |
| 5 | Gateway | Transaksi cocok: status menjadi paid, webhook dikirim, Telegram opsional. |
| 6 | Toko | Verifikasi signature, simpan transactionId, lalu selesaikan order. |
amount pada request adalah nominal dasar. Gateway menambahkan suffix 0–99 agar nominal unik. Gunakan data.amount dari response — bukan nominal request — untuk menampilkan QR dan mencocokkan pembayaran.
Ringkasan endpoint
| Method | Path | Auth | Fungsi |
|---|---|---|---|
GET | /docs | Tidak | Halaman dokumentasi ini. |
GET | / | Tidak | Halaman depan gateway. |
GET | /api/health | Tidak | Health check JSON. |
POST | /create-qris | Ya | Membuat string QRIS dinamis. |
POST | /check-payment | Ya | Membaca status pembayaran di store. |
POST | /cancel | Ya | Membatalkan pembayaran pending; menghentikan polling dan webhook. |
GET | /transactions | Ya | Transaksi sekitar 3 hari terakhir. |
GET | /transactions/all | Ya | Transaksi bulan berjalan. |
GET | /token-status | Ya | Validasi token Shopee. |
POST | /update-token | Ya | Ganti token Shopee saat runtime. |
GET | /api/logs | Ya | Ring buffer log in-memory. |
GET | /dashboard | Tidak | Shell dashboard transaksi (tanpa data). |
GET | /api/dashboard/overview | Ya | KPI, grafik harian, dan breakdown. |
GET | /api/dashboard/payments | Ya | Riwayat transaksi dengan filter dan pagination. |
Quick start
Butuh Bun 1.1 atau lebih baru. State pembayaran aktif disimpan di memori proses, sedangkan riwayat transaksi untuk dashboard ditulis ke SQLite lokal secara otomatis.
bun install
cp .env.example .env
# isi API_KEY, SHOPEE_TOKEN, dan QRIS_STATIC
bun run dev
Environment minimal
PORT=3001
API_KEY=minimal-32-karakter-acak
SHOPEE_TOKEN=B:token-dari-partner-portal
QRIS_STATIC=000201010211...
API_KEY minimal 32 karakter. Generate dengan openssl rand -hex 32. Token Shopee diambil dari Partner Portal: DevTools → Network → request get-transaction-list → data.metadata.token.
Cek gateway
curl http://localhost:3001/api/health
{
"success": true,
"message": "ShopeePay API Service is running",
"timestamp": "2026-10-31T12:00:00.000Z"
}
Halaman ini tersedia di /docs. Endpoint API yang dilindungi memerlukan header X-API-Key.
Autentikasi
Semua endpoint sensitif memakai header X-API-Key. API key pada query string tidak diterima.
curl http://localhost:3001/transactions \
-H "X-API-Key: $API_KEY"
Multi-store
Jika Anda mengelola beberapa merchant, kirim token Shopee per request melalui header X-Shopee-Token. Tanpa header ini, gateway memakai token default dari SHOPEE_TOKEN atau hasil POST /update-token.
curl http://localhost:3001/token-status \
-H "X-API-Key: $API_KEY" \
-H "X-Shopee-Token: B:token-merchant-lain"
Jika API key salah atau tidak ada, response berbentuk {"success":false,"error":"Invalid or missing API key"} dengan status 401.
POST /create-qris
Membuat QRIS dinamis. Gateway memilih suffix terendah yang belum dipakai untuk nominal dasar tersebut, lalu menyisipkan nominal final ke payload QRIS.
Request
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
amount | number | Ya | Nominal dasar dalam rupiah, harus > 0. |
order_id | string | Tidak | Maksimal 128 karakter. Dikembalikan di response dan payload webhook. |
webhook_url | string | Tidak | Maksimal 2048 karakter. Host harus cocok dengan WEBHOOK_ALLOWED_HOSTS. |
curl -X POST http://localhost:3001/create-qris \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"order_id": "ORD-2026-00123",
"webhook_url": "https://shop.example.com/payments/webhook"
}'
Response
{
"success": true,
"data": {
"qris": "00020101021226...6304A1B2",
"amount": 50017,
"order_id": "ORD-2026-00123",
"start_time": 1761912000,
"expires_at": "2026-10-31T12:15:00.000Z",
"expires_in_seconds": 900,
"poll_interval_seconds": 7,
"webhook_enabled": true
}
}
| Field | Tipe | Keterangan |
|---|---|---|
qris | string | Payload EMVCo. Render menjadi gambar QR di sisi client. |
amount | number | Nominal final yang harus dibayar, termasuk suffix. |
order_id | string | null | Echo dari request jika dikirim. |
start_time | number | Unix seconds, sudah dikurangi buffer clock skew 30 detik. |
expires_at | string | Waktu kedaluwarsa metadata dalam ISO 8601. |
expires_in_seconds | number | TTL dari QRIS_TTL_SECONDS. |
poll_interval_seconds | number | Saran interval polling client. |
webhook_enabled | boolean | True jika webhook per order atau WEBHOOK_URL aktif. |
Render QR
Gateway hanya mengembalikan string QRIS. Gambar QR dibuat oleh client, misalnya dengan library qrcode.
import QRCode from "qrcode";
await QRCode.toCanvas(canvasElement, data.qris);
Jika suffix 0–99 untuk nominal dasar yang sama sedang terpakai semua, request mengembalikan 400. Gunakan nominal dasar lain atau tunggu pembayaran lama kedaluwarsa.
webhook_url ditolak dengan 400 jika host-nya tidak ada di WEBHOOK_ALLOWED_HOSTS. Jika daftar kosong, gunakan WEBHOOK_URL global atau aktifkan allowlist terlebih dahulu.
POST /check-payment
Membaca status pembayaran dari store in-memory. Verifikasi transaksi tetap dilakukan oleh poller; endpoint ini tidak memanggil Shopee secara langsung.
curl -X POST http://localhost:3001/check-payment \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 50017}'
Gunakan nominal final dari /create-qris. Response saat belum lunas:
{
"success": true,
"paid": false
}
Response saat sudah lunas:
{
"success": true,
"paid": true,
"transaction": {
"transactionId": "TX123",
"amount": 50017,
"status": "success",
"time": "2026-10-31 19:00:05",
"issuer": "Seabank"
}
}
Status berubah menjadi paid setelah poller menemukan transaksi yang cocok, dengan interval default 7 detik. Record paid dipertahankan sampai 3 menit setelah expires_at. Tetap deduplikasi transactionId di database toko.
POST /cancel
Membatalkan pembayaran yang masih pending. Setelah dibatalkan, poller berhenti mengecek transaksi tersebut ke Shopee dan tidak akan mengirim webhook payment.paid. Gunakan nominal final dari /create-qris.
curl -X POST http://localhost:3001/cancel \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 50017, "order_id": "ORD-2026-00123"}'
{
"success": true,
"data": {
"amount": 50017,
"order_id": "ORD-2026-00123",
"status": "cancelled",
"already_cancelled": false
}
}
| Status | Kondisi |
|---|---|
200 | Pembayaran dibatalkan, atau sudah dibatalkan (already_cancelled: true). |
400 | amount tidak valid atau order_id bukan string. |
404 | Nominal tidak dikenal atau sudah kedaluwarsa. |
409 | Sudah lunas, atau order_id tidak cocok. |
Endpoint ini hanya menghentikan polling dan webhook di gateway. QRIS di sisi Shopee tidak dibatalkan, jadi pembeli yang sudah melihat QR masih mungkin membayarnya. Lakukan rekonsiliasi lewat /check-payment atau laporan transaksi.
Record cancelled dipertahankan sampai TTL habis agar nominal uniknya tidak dipakai ulang selama QR masih mungkin dibayar. Jika tidak ada pembayaran pending lain, poller langsung berhenti; POST /create-qris berikutnya menyalakannya lagi. Restart gateway menghilangkan state ini.
Transaksi
Dua endpoint untuk melihat transaksi langsung dari Shopee. Keduanya menerima X-Shopee-Token opsional.
| Endpoint | Cakupan | Page size |
|---|---|---|
GET /transactions | Sekitar 3 hari terakhir | 10 |
GET /transactions/all | Bulan berjalan (zona Jakarta) | 100 |
curl http://localhost:3001/transactions \
-H "X-API-Key: $API_KEY"
{
"success": true,
"total_amount": "150.017",
"data": {
"transactions": [
{
"transactionId": "TX123",
"amount": 50017,
"status": "success",
"time": "2026-10-31 19:00:05"
}
]
}
}
Jika token kedaluwarsa, response 401 menyertakan instruksi untuk memanggil POST /update-token. Error upstream lain dikembalikan sebagai 502.
Token
Token Shopee disimpan di memori. Setelah restart, gateway kembali memakai nilai SHOPEE_TOKEN dari environment.
GET /token-status
curl http://localhost:3001/token-status \
-H "X-API-Key: $API_KEY"
{
"success": true,
"data": {
"token_status": "valid",
"message": "Token is working"
}
}
POST /update-token
curl -X POST http://localhost:3001/update-token \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"token": "B:token-baru"}'
Token divalidasi sebelum disimpan. Jika validasi tidak memungkinkan, tambahkan "skipValidation": true untuk memaksa update.
Pemeriksaan token berjalan di background. Default setiap 30 menit, pemeriksaan pertama 30 detik setelah boot. Set TOKEN_CHECK_INTERVAL_MS=0 untuk menonaktifkan.
Log
GET /api/logs mengembalikan ring buffer log in-memory. Secret seperti API key dan token disamarkan sebelum ditampilkan.
curl http://localhost:3001/api/logs \
-H "X-API-Key: $API_KEY"
{
"success": true,
"data": {
"logs": [
{
"level": "info",
"message": "QRIS created for base Rp 50000 -> final Rp 50017 (suffix +17)",
"timestamp": "2026-10-31T12:00:00.000Z"
}
]
}
}
Dashboard
GET /dashboard menyajikan dashboard transaksi read-only. Shell HTML-nya publik dan tidak memuat data; Anda memasukkan API_KEY sekali, lalu key disimpan di sessionStorage tab tersebut dan dikirim lewat header X-API-Key. Key tidak pernah masuk URL atau query string.
Dashboard membaca ledger SQLite yang ditulis gateway saat QRIS dibuat, dibatalkan, atau lunas. Store pembayaran aktif tetap in-memory; ledger inilah yang bertahan setelah restart dan mengisi grafik 7/30/90 hari.
| Status | Arti |
|---|---|
paid | Pembayaran terverifikasi, lengkap dengan transactionId dan issuer. |
pending | QRIS masih berlaku (expires_at > sekarang). |
cancelled | Dibatalkan lewat POST /cancel. |
expired | Diturunkan, bukan disimpan: baris pending yang melewati expires_at (timeout TTL). |
Rentang yang didukung: 24h, 7d (default), 30d, dan 90d. Tanggal grafik dikelompokkan memakai zona Jakarta (UTC+7). Total KPI mengikuti rentang yang dipilih; pending adalah snapshot saat ini untuk QRIS yang masih dalam TTL, dan breakdown status mencerminkan seluruh ledger.
curl "http://localhost:3001/api/dashboard/overview?range=7d" \
-H "X-API-Key: $API_KEY"
curl "http://localhost:3001/api/dashboard/payments?status=paid&limit=50" \
-H "X-API-Key: $API_KEY"
overview mengembalikan total KPI (paid_count, paid_amount, pending_count, pending_amount, cancelled_count, expired_count, created_count, success_rate, average_ticket), series harian, status_breakdown, dan issuers.
payments mengembalikan maksimum 200 baris (default 50), terbaru lebih dulu, dengan next_cursor. Filter yang tersedia: status, range, q (mencari order_id atau transaction_id), limit, dan cursor.
{
"success": true,
"data": {
"payments": [
{
"id": 42,
"order_id": "ORD-2026-00123",
"base_amount": 50000,
"suffix": 17,
"final_amount": 50017,
"status": "paid",
"created_at": 1793491200,
"expires_at": 1793492100,
"paid_at": 1793491260,
"transaction_id": "TX123",
"issuer": "Seabank"
}
],
"next_cursor": "40"
}
}
Lokasi file diatur DATABASE_PATH (default ./data/gateway.db); direktorinya dibuat otomatis dan data/ diabaikan git. Retensi diatur DASHBOARD_RETENTION_DAYS (default 90 hari) dan dibersihkan saat start. SQLite ditujukan untuk satu instance gateway: beberapa instance akan memecah ledger, jadi pindah ke Postgres/Redis sebelum scale-out.
Status cancelled hanya menandai baris ledger dan menghentikan polling. QRIS tidak dibatalkan di Shopee, dan dashboard menampilkan data lokal gateway — bukan seluruh transaksi akun merchant.
Webhook payment.paid
Webhook dikirim satu kali ketika poller memverifikasi pembayaran dan markPaid() berhasil. Client tidak perlu memanggil /check-payment untuk memicu webhook.
Kapan dikirim
- Transaksi dari Shopee berstatus sukses dan nominalnya cocok dengan record pending.
- Record belum pernah berstatus
paid. - Pengiriman berjalan asynchronous, jadi tidak memperlambat siklus polling.
Tujuan pengiriman
| Prioritas | Sumber URL | Aturan |
|---|---|---|
| 1 | webhook_url per order | Host harus cocok dengan WEBHOOK_ALLOWED_HOSTS. |
| 2 | WEBHOOK_URL | Fallback global, dikendalikan operator. |
| 3 | Tidak ada | Webhook tidak dikirim; client tetap bisa polling. |
Environment webhook
| Variable | Default | Keterangan |
|---|---|---|
WEBHOOK_URL | kosong | URL global untuk semua pembayaran. |
WEBHOOK_SECRET | kosong | Secret HMAC-SHA256. Kosong berarti request tidak ditandatangani. |
WEBHOOK_ALLOWED_HOSTS | kosong | Daftar host dipisah koma. Subdomain dan wildcard *.domain didukung. |
WEBHOOK_TIMEOUT_MS | 10000 | Timeout per percobaan. |
WEBHOOK_MAX_RETRIES | 3 | Percobaan tambahan setelah percobaan pertama. |
WEBHOOK_RETRY_BASE_MS | 2000 | Basis backoff eksponensial. |
Payload
{
"event": "payment.paid",
"event_id": "0f1c2d3e-4b5a-4c6d-8e9f-123456789abc",
"created_at": "2026-10-31T12:00:05.000Z",
"data": {
"order_id": "ORD-2026-00123",
"amount": 50017,
"base_amount": 50000,
"suffix": 17,
"status": "paid",
"paid_at": "2026-10-31T12:00:05.000Z",
"transaction": {
"transactionId": "TX123",
"amount": 50017,
"status": "success",
"time": "2026-10-31 19:00:05",
"issuer": "Seabank"
}
}
}
| Field | Keterangan |
|---|---|
event | Selalu payment.paid. |
event_id | UUID unik. Nilainya sama untuk semua retry event yang sama. |
data.order_id | order_id dari request /create-qris, jika ada. |
data.amount | Nominal final yang dibayar. |
data.base_amount | Nominal dasar dari request. |
data.suffix | Suffix 0–99 yang dipilih gateway. |
data.transaction | Detail transaksi dari Shopee. |
Header & signature
| Header | Isi |
|---|---|
Content-Type | application/json |
X-Webhook-Event | payment.paid |
X-Webhook-Id | Event ID; sama saat retry. |
X-Webhook-Timestamp | Unix seconds. |
X-Webhook-Signature | sha256=<hex> jika WEBHOOK_SECRET diisi. |
Signature adalah HMAC-SHA256 dari timestamp + "." + rawBody. Verifikasi memakai raw body persis seperti yang diterima, bukan hasil parse lalu stringify ulang.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody, headers, secret) {
const timestamp = headers["x-webhook-timestamp"];
const received = (headers["x-webhook-signature"] || "").replace(/^sha256=/, "");
const expected = createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return (
fresh &&
received.length === expected.length &&
timingSafeEqual(Buffer.from(received), Buffer.from(expected))
);
}
Tolak timestamp yang lebih lama dari beberapa menit, lalu deduplikasi event_id atau transaction.transactionId. Jangan menyimpan WEBHOOK_SECRET di browser.
Retry, idempotensi, keamanan
| Kondisi | Perilaku gateway |
|---|---|
| Response 2xx | Dianggap sukses, tidak ada retry. |
| Network error, 429, 5xx | Retry dengan exponential backoff sampai WEBHOOK_MAX_RETRIES. |
| 4xx, redirect, respons non-2xx lain | Permanen, tidak di-retry. |
| Timeout | Dianggap gagal; masuk jalur retry. |
Semantik pengiriman adalah at-least-once. Jika endpoint Anda sudah memproses event tetapi response gagal diterima gateway, event yang sama bisa dikirim lagi. Karena itu, selalu deduplikasi.
URL dari client hanya diterima jika host-nya ada di WEBHOOK_ALLOWED_HOSTS. Redirect tidak diikuti. URL dengan kredensial dan skema selain HTTP/HTTPS ditolak.
Antrian retry masih in-memory. Restart gateway di antara percobaan akan menghilangkan retry yang belum selesai. Untuk order kritikal, tetap rekonsiliasi lewat /check-payment atau bangun outbox persisten.
Checklist integrasi
- Verifikasi signature sebelum memproses payload.
- Balas 2xx secepat mungkin; proses bisnis boleh dilakukan setelah response.
- Deduplikasi berdasarkan
event_idatautransactionId. - Cocokkan
order_iddan nominal dengan order di database. - Sediakan jalur rekonsiliasi manual atau polling.
Konfigurasi
Semua konfigurasi dibaca dari environment saat proses start. Lihat .env.example untuk daftar lengkap.
| Variable | Default | Keterangan |
|---|---|---|
PORT | 3001 | Port HTTP gateway. |
API_KEY | wajib | Minimal 32 karakter. |
SHOPEE_TOKEN | wajib | Token awal, biasanya diawali B:. |
QRIS_STATIC | wajib | Payload QRIS statis milik merchant. |
CORS_ORIGINS | kosong | Allowlist dipisah koma. Kosong berarti browser CORS ditolak. |
TRUST_PROXY | false | Aktifkan jika berada di belakang reverse proxy tepercaya. |
RATE_LIMIT_WINDOW_MS | 60000 | Jendela rate limit. |
RATE_LIMIT_MAX | 60 | Maksimum request per jendela. |
QRIS_TTL_SECONDS | 900 | TTL metadata QRIS. |
POLL_INTERVAL_SECONDS | 7 | Interval poller memeriksa Shopee. |
TOKEN_CHECK_INTERVAL_MS | 1800000 | Watcher token. 0 menonaktifkan. |
TELEGRAM_BOT_TOKEN | kosong | Token bot Telegram, opsional. |
TELEGRAM_CHAT_ID | kosong | Chat tujuan notifikasi. |
TELEGRAM_ON_PAID | true | 0 mematikan notifikasi pembayaran. |
WEBHOOK_URL | kosong | URL webhook global. |
WEBHOOK_SECRET | kosong | Secret HMAC-SHA256. |
WEBHOOK_ALLOWED_HOSTS | kosong | Host yang boleh dipakai untuk webhook_url per order. |
WEBHOOK_TIMEOUT_MS | 10000 | Timeout per percobaan webhook. |
WEBHOOK_MAX_RETRIES | 3 | Percobaan tambahan setelah percobaan pertama. |
WEBHOOK_RETRY_BASE_MS | 2000 | Basis backoff. |
DEDUP_TTL_MS | 86400000 | TTL klaim transactionId in-memory. |
DATABASE_PATH | ./data/gateway.db | Lokasi file ledger SQLite untuk dashboard. |
DASHBOARD_RETENTION_DAYS | 90 | Retensi riwayat transaksi, dibersihkan saat start. |
LOG_LEVEL | info | debug, info, warn, atau error. |
Error & limit
| Status | Arti | Yang harus dilakukan |
|---|---|---|
400 | Validasi request gagal, suffix habis, atau webhook URL ditolak. | Baca field error pada response. |
401 | API key salah atau token Shopee kedaluwarsa. | Cek X-API-Key; perbarui token lewat /update-token. |
404 | Path tidak ada. | Cek daftar endpoint. |
413 | Body request lebih dari 16 KB. | Kirim payload yang lebih kecil. |
429 | Rate limit terlampaui. | Tunggu jendela berikutnya atau naikkan limit. |
500 | Error internal yang tidak tertangani. | Cek /api/logs. |
502 | Shopee tidak dapat dihubungi atau mengembalikan error upstream. | Coba lagi; cek status token. |
Batas body request adalah 16 KB. Rate limit default 60 request per 60 detik.
Jika TRUST_PROXY tidak aktif, gateway tidak membaca X-Forwarded-For; semua request masuk ke bucket unknown. Akibatnya limit berlaku untuk total trafik, bukan per IP. Aktifkan TRUST_PROXY=1 hanya jika Anda berada di belakang proxy tepercaya.
Checklist produksi
- Pasang TLS di reverse proxy; jangan ekspos gateway tanpa HTTPS.
- Gunakan
API_KEYacak minimal 32 karakter dan rotasi secara berkala. - Set
TRUST_PROXY=1hanya di belakang proxy yang Anda kendalikan. - Batasi
CORS_ORIGINSke domain toko yang benar-benar dipakai. - Isi
WEBHOOK_SECRETdan aktifkanWEBHOOK_ALLOWED_HOSTSuntuk URL per order. - Simpan
transactionIddanevent_idunik di database toko. - Sediakan rekonsiliasi lewat
/check-paymentatau laporan transaksi. - Pantau
/api/health,/api/logs, dan peringatan Telegram. - Pahami bahwa ini gateway tidak resmi dan dapat terpengaruh perubahan API Shopee.
Payment store, dedup, rate limit, dan antrian webhook hilang saat proses restart. Riwayat transaksi dashboard tetap tersimpan di ledger SQLite, tetapi state pembayaran aktif tidak. Untuk produksi multi-instance atau jaminan delivery, tambahkan penyimpanan persisten dan worker terpisah.