Error, idempotensi & retry
Tangani error secara terstruktur dan pulihkan request tanpa membuat pembelian ganda, mengubah nominal, atau melepaskan hold yang belum aman.
Unduh OpenAPI 3.1Di halaman ini
Periksa HTTP dan envelope
{
"ok": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Saldo tersedia belum cukup untuk nomor ini."
}
}Gunakan error.code untuk cabang program, error.message untuk pesan pengguna. Jangan bergantung pada terjemahan message. RequestError dari api() menyimpan code, message, dan status. NETWORK_ERROR dengan status 0 berasal dari client, bukan status HTTP server.
import { RequestError } from "@/lib/ping/client";
function classifyFailure(error: unknown) {
if (!(error instanceof RequestError)) return "inspect";
if (error.code === "UNAUTHENTICATED") return "sign-in";
if (["PRICE_CHANGED", "PRICE_OPTION_UNAVAILABLE"].includes(error.code)) {
return "refresh-quote-and-confirm";
}
if (error.code === "INSUFFICIENT_BALANCE") return "top-up";
if (error.status === 0 || error.status >= 500) return "check-existing-intent";
return "show-error-and-refresh";
}UUID milik satu niat transaksi
Idempotensi order dan top-up memakai clientRequestId pada body JSON. Header Idempotency-Key tidak dibaca oleh kedua route ini. Buat UUID sekali sebelum request pertama, simpan bersama pilihan dan nominal pending, lalu jangan ganti nilainya karena error jaringan.
| Operasi | Perilaku replay |
|---|---|
| POST /api/orders | Unik per userId + clientRequestId. Replay mengembalikan order lama sebelum membuat hold/pembelian baru. Payload yang berbeda tidak membuat intent baru dan tidak otomatis menghasilkan IDEMPOTENCY_CONFLICT; jangan reuse UUID untuk pilihan berbeda. |
| POST /api/payments/klikqris | UUID yang sama dan nominal sama mengembalikan pembayaran awal. Nominal berbeda menghasilkan IDEMPOTENCY_CONFLICT. |
| Pembayaran aktif lain | Hanya satu CREATING/PENDING per user. Nominal sama dapat mengembalikan pembayaran aktif; nominal lain dapat menghasilkan CONFLICT. |
| Cancel / complete | Tidak menerima clientRequestId. Settlement memakai kunci server tetap per order. Panggilan berulang tetap tunduk pada status; jangan mengharapkan setiap pengulangan menghasilkan 200. |
Saat respons hilang
Jangan mengartikan timeout sebagai gagal beli
Provider mungkin sudah menerbitkan nomor atau QR walaupun respons tidak sampai. Jangan mengganti UUID, membuat top-up lain, atau membebaskan hold di client.
Pulihkan resource yang ada
Jika ID diketahui, baca detail. Untuk top-up, GET pembayaran aktif. Untuk order, tampilkan daftar milik akun; jika UUID awal masih diketahui, recovery request harus memakai UUID dan payload yang sama, bukan retry otomatis dengan intent baru.
Tunggu rekonsiliasi bila masih ambigu
Jika message menyebut konfirmasi provider atau error PURCHASE_UNRESOLVED/RECONCILIATION_REQUIRED, lanjutkan pembacaan terbatas dan biarkan scheduler/provider menyelesaikan hasilnya.
Mulai intent baru hanya setelah jelas
Jika pengguna benar-benar memilih transaksi baru setelah hasil sebelumnya diketahui, baru buat UUID baru. Perubahan harga memerlukan konfirmasi ulang.
Referensi error yang relevan
Status berikut berasal dari pemetaan apiHandler. Endpoint webhook dapat memberi override status untuk redelivery; contohnya RECONCILIATION_REQUIRED pada webhook Tiger menjadi 503. Tidak semua kode muncul pada setiap endpoint.
| Kode | HTTP | Makna | Penanganan |
|---|---|---|---|
| BAD_REQUEST | 400 | Body JSON atau Content-Type tidak valid. | Perbaiki request; untuk aksi cancel/complete, kirim {}. |
| VALIDATION_ERROR | 400 | Input tidak sesuai schema. | Periksa tipe, field wajib, format UUID, dan field tambahan. |
| UNAUTHENTICATED | 401 | Sesi tidak tersedia atau sudah berakhir. | Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis. |
| FORBIDDEN | 403 | Origin atau izin akses ditolak. | Periksa origin dan role; jangan menonaktifkan guard. |
| ACCOUNT_SUSPENDED | 403 | Akun ditangguhkan. | Hentikan operasi dan hubungi pengelola. |
| NOT_FOUND | 404 | Resource tidak ditemukan atau bukan milik sesi. | Periksa ID dan akun; ID resource tidak memberi akses lintas pengguna. |
| PRICE_CHANGED | 409 | Harga saat pembelian berbeda dari konfirmasi. | Ambil quote baru dan minta konfirmasi harga lagi. |
| PRICE_OPTION_UNAVAILABLE | 409 | Pilihan harga sudah tidak tersedia. | Ambil quote baru; jangan menebak atau membuat priceOptionId. |
| EARLY_CANCEL_DENIED | 409 | Belum melewati batas pembatalan. | Baca ulang order dan ikuti canCancel serta cancelAvailableAt. |
| INVALID_STATE | 409 | Aksi tidak cocok dengan status terkini. | GET detail order sebelum menawarkan aksi kembali. |
| INVALID_TRANSITION | 409 | Transisi status tidak diizinkan. | Muat ulang order; jangan memaksa perubahan status. |
| ORDER_ALREADY_TERMINAL | 409 | Order telah berakhir. | Tampilkan hasil terkini dan perbarui saldo. |
| PURCHASE_UNRESOLVED | 409 | Masih ada pembelian yang hasilnya belum pasti. | Jangan membuat pembelian baru. Tunggu rekonsiliasi. |
| RECONCILIATION_REQUIRED | 409 | Hasil provider perlu dikonfirmasi. | Pertahankan hold; baca ulang status. Webhook Tiger dapat memakai HTTP 503 untuk kode ini. |
| IDEMPOTENCY_CONFLICT | 409 | UUID top-up sudah dipakai untuk nominal lain. | Pulihkan pembayaran awal. UUID baru hanya untuk niat transaksi baru. |
| CONFLICT | 409 | Ada pembayaran aktif atau detail provider tidak cocok. | Baca pembayaran aktif, jangan terus membuat top-up baru. |
| INSUFFICIENT_BALANCE | 422 | Saldo tersedia tidak cukup. | Tampilkan available dari server dan tawarkan top-up. |
| INVALID_AMOUNT | 422 | Nominal tidak valid untuk operasi saldo. | Periksa integer IDR. Validasi body top-up sendiri memakai VALIDATION_ERROR / 400. |
| SERVICE_UNAVAILABLE | 422 | Layanan tidak aktif atau tidak ditemukan. | Muat ulang katalog dan pilih layanan yang tersedia. |
| COUNTRY_UNAVAILABLE | 422 | Negara tidak aktif atau tidak ditemukan. | Muat ulang katalog dan pilih negara yang tersedia. |
| PRICE_UNAVAILABLE | 422 | Quote/nomor tidak tersedia untuk pasangan ini. | Tawarkan pasangan lain atau coba baca quote lagi nanti. |
| ACTIVE_ORDER_LIMIT | 429 | Batas order aktif tercapai. | Selesaikan order yang ada; limit mengikuti pengaturan operator. |
| ORDER_RATE_LIMIT | 429 | Terlalu banyak percobaan pembelian. | Jeda percobaan. Jangan membuat loop POST. |
| RATE_LIMITED | 429 | Batas permintaan tercapai. | Gunakan backoff. Jangan mengasumsikan header kuota atau Retry-After selalu ada. |
| INTERNAL_ERROR | 500 | Kegagalan internal yang disanitasi. | Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal. |
| PROVIDER_ERROR | 502 | Provider belum dapat memproses permintaan. | Baca ulang status. Jangan retry pembelian upstream secara otomatis. |
| PAYMENT_PROVIDER_ERROR | 502 | Gangguan penyedia pembayaran. | Baca pembayaran aktif atau detail dengan ID yang sama. |
| PROVIDER_UNAVAILABLE | 503 | Provider tidak dapat dihubungi atau belum disiapkan. | Periksa konfigurasi server; lanjutkan pembacaan dengan backoff. |
| PAYMENT_NOT_CONFIGURED | 503 | Pembayaran belum dikonfigurasi. | Pemilik instalasi harus menyiapkan KlikQRIS; bukan mengganti data dengan demo. |
| PAYMENT_NOT_READY | 503 | Pembayaran belum siap diverifikasi. | Biarkan provider mengirim ulang callback. Jangan percaya signature baru dari callback. |
| PROVIDER_TIMEOUT | 504 | Respons provider melewati batas waktu. | Hasil bisa ambigu. Pertahankan UUID dan hold, tunggu rekonsiliasi. |
Limit mengikuti operator, bukan paket API publik
Batas jumlah order aktif, frekuensi pembelian, provider budget, dan backoff adalah pengaturan/koordinasi server. Jangan menulis asumsi kuota tetap atau menjanjikan X-RateLimit/Retry-After pada semua error. Retry baca harus terbatas dan berhenti pada 401/403/404 yang tidak bisa dipulihkan otomatis.