Beranda Dokumentasi API v3.0.0

Dokumentasi API

Gateway QRIS untuk membuat kode bayar, memverifikasi transaksi ShopeePay, dan mengirim webhook saat pembayaran lunas.

Base URL 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.

TahapAktorYang terjadi
1TokoTentukan nominal dasar dan order_id, lalu panggil POST /create-qris.
2GatewayMemilih suffix unik 0–99, membentuk nominal final, dan mengembalikan string QRIS.
3PelangganScan QRIS dan membayar nominal final.
4GatewayPoller memeriksa transaksi ShopeePay setiap POLL_INTERVAL_SECONDS.
5GatewayTransaksi cocok: status menjadi paid, webhook dikirim, Telegram opsional.
6TokoVerifikasi signature, simpan transactionId, lalu selesaikan order.
Nominal final

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

MethodPathAuthFungsi
GET/docsTidakHalaman dokumentasi ini.
GET/TidakHalaman depan gateway.
GET/api/healthTidakHealth check JSON.
POST/create-qrisYaMembuat string QRIS dinamis.
POST/check-paymentYaMembaca status pembayaran di store.
POST/cancelYaMembatalkan pembayaran pending; menghentikan polling dan webhook.
GET/transactionsYaTransaksi sekitar 3 hari terakhir.
GET/transactions/allYaTransaksi bulan berjalan.
GET/token-statusYaValidasi token Shopee.
POST/update-tokenYaGanti token Shopee saat runtime.
GET/api/logsYaRing buffer log in-memory.
GET/dashboardTidakShell dashboard transaksi (tanpa data).
GET/api/dashboard/overviewYaKPI, grafik harian, dan breakdown.
GET/api/dashboard/paymentsYaRiwayat 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.

bash
bun install
cp .env.example .env
# isi API_KEY, SHOPEE_TOKEN, dan QRIS_STATIC
bun run dev

Environment minimal

env
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

bash
curl http://localhost:3001/api/health
json
{
  "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.

bash
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.

bash
curl http://localhost:3001/token-status \
  -H "X-API-Key: $API_KEY" \
  -H "X-Shopee-Token: B:token-merchant-lain"
401

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

FieldTipeWajibKeterangan
amountnumberYaNominal dasar dalam rupiah, harus > 0.
order_idstringTidakMaksimal 128 karakter. Dikembalikan di response dan payload webhook.
webhook_urlstringTidakMaksimal 2048 karakter. Host harus cocok dengan WEBHOOK_ALLOWED_HOSTS.
bash
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

json
{
  "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
  }
}
FieldTipeKeterangan
qrisstringPayload EMVCo. Render menjadi gambar QR di sisi client.
amountnumberNominal final yang harus dibayar, termasuk suffix.
order_idstring | nullEcho dari request jika dikirim.
start_timenumberUnix seconds, sudah dikurangi buffer clock skew 30 detik.
expires_atstringWaktu kedaluwarsa metadata dalam ISO 8601.
expires_in_secondsnumberTTL dari QRIS_TTL_SECONDS.
poll_interval_secondsnumberSaran interval polling client.
webhook_enabledbooleanTrue jika webhook per order atau WEBHOOK_URL aktif.

Render QR

Gateway hanya mengembalikan string QRIS. Gambar QR dibuat oleh client, misalnya dengan library qrcode.

js
import QRCode from "qrcode";

await QRCode.toCanvas(canvasElement, data.qris);
Suffix habis

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 per order

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.

bash
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:

json
{
  "success": true,
  "paid": false
}

Response saat sudah lunas:

json
{
  "success": true,
  "paid": true,
  "transaction": {
    "transactionId": "TX123",
    "amount": 50017,
    "status": "success",
    "time": "2026-10-31 19:00:05",
    "issuer": "Seabank"
  }
}
Polling sebagai fallback

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.

bash
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"}'
json
{
  "success": true,
  "data": {
    "amount": 50017,
    "order_id": "ORD-2026-00123",
    "status": "cancelled",
    "already_cancelled": false
  }
}
StatusKondisi
200Pembayaran dibatalkan, atau sudah dibatalkan (already_cancelled: true).
400amount tidak valid atau order_id bukan string.
404Nominal tidak dikenal atau sudah kedaluwarsa.
409Sudah lunas, atau order_id tidak cocok.
Bukan refund

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.

State in-memory

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.

EndpointCakupanPage size
GET /transactionsSekitar 3 hari terakhir10
GET /transactions/allBulan berjalan (zona Jakarta)100
bash
curl http://localhost:3001/transactions \
  -H "X-API-Key: $API_KEY"
json
{
  "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

bash
curl http://localhost:3001/token-status \
  -H "X-API-Key: $API_KEY"
json
{
  "success": true,
  "data": {
    "token_status": "valid",
    "message": "Token is working"
  }
}

POST /update-token

bash
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.

Watcher token

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.

bash
curl http://localhost:3001/api/logs \
  -H "X-API-Key: $API_KEY"
json
{
  "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.

StatusArti
paidPembayaran terverifikasi, lengkap dengan transactionId dan issuer.
pendingQRIS masih berlaku (expires_at > sekarang).
cancelledDibatalkan lewat POST /cancel.
expiredDiturunkan, 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.

bash
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.

json
{
  "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"
  }
}
Penyimpanan ledger

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.

Bukan refund

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

PrioritasSumber URLAturan
1webhook_url per orderHost harus cocok dengan WEBHOOK_ALLOWED_HOSTS.
2WEBHOOK_URLFallback global, dikendalikan operator.
3Tidak adaWebhook tidak dikirim; client tetap bisa polling.

Environment webhook

VariableDefaultKeterangan
WEBHOOK_URLkosongURL global untuk semua pembayaran.
WEBHOOK_SECRETkosongSecret HMAC-SHA256. Kosong berarti request tidak ditandatangani.
WEBHOOK_ALLOWED_HOSTSkosongDaftar host dipisah koma. Subdomain dan wildcard *.domain didukung.
WEBHOOK_TIMEOUT_MS10000Timeout per percobaan.
WEBHOOK_MAX_RETRIES3Percobaan tambahan setelah percobaan pertama.
WEBHOOK_RETRY_BASE_MS2000Basis backoff eksponensial.

Payload

json
{
  "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"
    }
  }
}
FieldKeterangan
eventSelalu payment.paid.
event_idUUID unik. Nilainya sama untuk semua retry event yang sama.
data.order_idorder_id dari request /create-qris, jika ada.
data.amountNominal final yang dibayar.
data.base_amountNominal dasar dari request.
data.suffixSuffix 0–99 yang dipilih gateway.
data.transactionDetail transaksi dari Shopee.

Header & signature

HeaderIsi
Content-Typeapplication/json
X-Webhook-Eventpayment.paid
X-Webhook-IdEvent ID; sama saat retry.
X-Webhook-TimestampUnix seconds.
X-Webhook-Signaturesha256=<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.

js
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))
  );
}
Replay protection

Tolak timestamp yang lebih lama dari beberapa menit, lalu deduplikasi event_id atau transaction.transactionId. Jangan menyimpan WEBHOOK_SECRET di browser.

Retry, idempotensi, keamanan

KondisiPerilaku gateway
Response 2xxDianggap sukses, tidak ada retry.
Network error, 429, 5xxRetry dengan exponential backoff sampai WEBHOOK_MAX_RETRIES.
4xx, redirect, respons non-2xx lainPermanen, tidak di-retry.
TimeoutDianggap 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.

Anti-SSRF

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.

Batasan delivery

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

  1. Verifikasi signature sebelum memproses payload.
  2. Balas 2xx secepat mungkin; proses bisnis boleh dilakukan setelah response.
  3. Deduplikasi berdasarkan event_id atau transactionId.
  4. Cocokkan order_id dan nominal dengan order di database.
  5. Sediakan jalur rekonsiliasi manual atau polling.

Konfigurasi

Semua konfigurasi dibaca dari environment saat proses start. Lihat .env.example untuk daftar lengkap.

VariableDefaultKeterangan
PORT3001Port HTTP gateway.
API_KEYwajibMinimal 32 karakter.
SHOPEE_TOKENwajibToken awal, biasanya diawali B:.
QRIS_STATICwajibPayload QRIS statis milik merchant.
CORS_ORIGINSkosongAllowlist dipisah koma. Kosong berarti browser CORS ditolak.
TRUST_PROXYfalseAktifkan jika berada di belakang reverse proxy tepercaya.
RATE_LIMIT_WINDOW_MS60000Jendela rate limit.
RATE_LIMIT_MAX60Maksimum request per jendela.
QRIS_TTL_SECONDS900TTL metadata QRIS.
POLL_INTERVAL_SECONDS7Interval poller memeriksa Shopee.
TOKEN_CHECK_INTERVAL_MS1800000Watcher token. 0 menonaktifkan.
TELEGRAM_BOT_TOKENkosongToken bot Telegram, opsional.
TELEGRAM_CHAT_IDkosongChat tujuan notifikasi.
TELEGRAM_ON_PAIDtrue0 mematikan notifikasi pembayaran.
WEBHOOK_URLkosongURL webhook global.
WEBHOOK_SECRETkosongSecret HMAC-SHA256.
WEBHOOK_ALLOWED_HOSTSkosongHost yang boleh dipakai untuk webhook_url per order.
WEBHOOK_TIMEOUT_MS10000Timeout per percobaan webhook.
WEBHOOK_MAX_RETRIES3Percobaan tambahan setelah percobaan pertama.
WEBHOOK_RETRY_BASE_MS2000Basis backoff.
DEDUP_TTL_MS86400000TTL klaim transactionId in-memory.
DATABASE_PATH./data/gateway.dbLokasi file ledger SQLite untuk dashboard.
DASHBOARD_RETENTION_DAYS90Retensi riwayat transaksi, dibersihkan saat start.
LOG_LEVELinfodebug, info, warn, atau error.

Error & limit

StatusArtiYang harus dilakukan
400Validasi request gagal, suffix habis, atau webhook URL ditolak.Baca field error pada response.
401API key salah atau token Shopee kedaluwarsa.Cek X-API-Key; perbarui token lewat /update-token.
404Path tidak ada.Cek daftar endpoint.
413Body request lebih dari 16 KB.Kirim payload yang lebih kecil.
429Rate limit terlampaui.Tunggu jendela berikutnya atau naikkan limit.
500Error internal yang tidak tertangani.Cek /api/logs.
502Shopee 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.

TRUST_PROXY dan rate limit

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

  1. Pasang TLS di reverse proxy; jangan ekspos gateway tanpa HTTPS.
  2. Gunakan API_KEY acak minimal 32 karakter dan rotasi secara berkala.
  3. Set TRUST_PROXY=1 hanya di belakang proxy yang Anda kendalikan.
  4. Batasi CORS_ORIGINS ke domain toko yang benar-benar dipakai.
  5. Isi WEBHOOK_SECRET dan aktifkan WEBHOOK_ALLOWED_HOSTS untuk URL per order.
  6. Simpan transactionId dan event_id unik di database toko.
  7. Sediakan rekonsiliasi lewat /check-payment atau laporan transaksi.
  8. Pantau /api/health, /api/logs, dan peringatan Telegram.
  9. Pahami bahwa ini gateway tidak resmi dan dapat terpengaruh perubahan API Shopee.
State in-memory

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.