{"openapi":"3.1.0","info":{"title":"Ping! Internal Application API","version":"0.1.0","description":"Kontrak API berbasis sesi untuk developer instalasi Ping!, bukan API publik reseller. Version mengikuti aplikasi, bukan prefix route /v1. Semua contoh adalah fixture dokumentasi. Login dilakukan melalui Better Auth pada origin instalasi yang sama. Endpoint auth, admin, health, scheduler, dan webhook provider tidak termasuk spesifikasi ini. Tidak ada webhook keluar ke pelanggan. Parameter dan aturan bisnis mengikuti source code rilis yang digunakan."},"servers":[{"url":"/","description":"Origin instalasi Ping! tempat spesifikasi disajikan; ganti dengan origin instalasi sendiri jika diimpor sebagai file."}],"security":[{"sessionCookie":[]},{"secureSessionCookie":[]}],"tags":[{"name":"akun","description":"Identitas dan preferensi pengguna."},{"name":"katalog","description":"Katalog aktif dan penawaran harga."},{"name":"pesanan","description":"Pembelian, SMS, pembatalan, dan penyelesaian."},{"name":"dompet","description":"Saldo dan ledger pengguna."},{"name":"pembayaran","description":"Top-up QRIS idempotent."}],"paths":{"/api/me":{"get":{"operationId":"getMe","tags":["akun"],"summary":"Baca akun & saldo","description":"Mengembalikan identitas sesi dan ringkasan dompet. username dapat null. role berasal dari server; client tidak dapat menentukan role sendiri.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/me/route.ts","responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Me"}}},"example":{"ok":true,"data":{"user":{"id":"user-example","name":"Developer","username":"developer","email":"developer@example.com","role":"user"},"balance":50000,"held":1500,"available":48500}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"patch":{"operationId":"updateProfile","tags":["akun"],"summary":"Ubah profil","description":"Memperbarui nama dan username. Kedua field wajib dikirim bersama. Endpoint ini bukan untuk mengganti email, password, role, atau userId.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/me/route.ts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfile"},"example":{"name":"Developer","username":"developer"}}}},"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Profile"}}},"example":{"ok":true,"data":{"name":"Developer","username":"developer","email":"developer@example.com"}}}}},"400":{"description":"BAD_REQUEST: Body JSON atau Content-Type tidak valid. Perbaiki request; untuk aksi cancel/complete, kirim {}.\nVALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"BAD_REQUEST":{"summary":"Body JSON atau Content-Type tidak valid.","value":{"ok":false,"error":{"code":"BAD_REQUEST","message":"Body JSON atau Content-Type tidak valid."}}},"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/profile/settings":{"get":{"operationId":"getSettings","tags":["akun"],"summary":"Baca preferensi","description":"Membaca preferensi akun. Jika belum ada pengaturan tersimpan, notifications bernilai true dan compact bernilai false.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/profile/settings/route.ts","responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/UserSettings"}}},"example":{"ok":true,"data":{"notifications":true,"compact":false}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"patch":{"operationId":"updateSettings","tags":["akun"],"summary":"Simpan preferensi","description":"Kirim minimal satu boolean. Field yang tidak dikirim tetap dipertahankan; body {} dan field tambahan ditolak.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/profile/settings/route.ts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettings"},"example":{"compact":true}}}},"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/UserSettings"}}},"example":{"ok":true,"data":{"notifications":true,"compact":true}}}}},"400":{"description":"BAD_REQUEST: Body JSON atau Content-Type tidak valid. Perbaiki request; untuk aksi cancel/complete, kirim {}.\nVALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"BAD_REQUEST":{"summary":"Body JSON atau Content-Type tidak valid.","value":{"ok":false,"error":{"code":"BAD_REQUEST","message":"Body JSON atau Content-Type tidak valid."}}},"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/catalog/services":{"get":{"operationId":"listServices","tags":["katalog"],"summary":"Daftar layanan","description":"Mengembalikan layanan yang diaktifkan operator. Respons berupa array tanpa pagination. Filter kategori/pencarian pada UI dilakukan client-side, bukan lewat parameter endpoint ini.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/catalog/services/route.ts","responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}},"example":{"ok":true,"data":[{"id":"service-example","provider":"tiger","name":"WhatsApp","icon":"whatsapp","category":"Pesan","description":"Nomor untuk satu aktivasi."}]}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/catalog/countries":{"get":{"operationId":"listCountries","tags":["katalog"],"summary":"Daftar negara","description":"Mengembalikan negara aktif, diurutkan berdasarkan nama tersimpan. Gunakan id dari respons untuk quote dan order, bukan iso2 atau kode panggilan.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/catalog/countries/route.ts","responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"type":"array","items":{"$ref":"#/components/schemas/Country"}}}},"example":{"ok":true,"data":[{"id":"country-example","provider":"tiger","name":"Indonesia","flag":"ID","dial":"+62","iso2":"ID"}]}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/catalog/quotes":{"get":{"operationId":"getQuote","tags":["katalog"],"summary":"Ambil penawaran harga","description":"Membaca quote satu pasangan layanan/negara, dengan refresh cache provider bila diperlukan. Kedua parameter wajib; parameter query tambahan ditolak. Harga dan opsi tetap divalidasi lagi saat pembelian.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/catalog/quotes/route.ts","parameters":[{"name":"serviceId","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":128,"description":"Service.id dari katalog."},"example":"service-example","description":"Service.id dari katalog."},{"name":"countryId","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":128,"description":"Country.id dari katalog."},"example":"country-example","description":"Country.id dari katalog."}],"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Quote"}}},"example":{"ok":true,"data":{"provider":"tiger","serviceId":"service-example","countryId":"country-example","price_idr":1500,"qualitative_stock":"available","options":[{"id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","price_idr":1500,"qualitative_stock":"available"}],"fetched_at":"2026-09-23T00:00:00.000Z","expires_at":"2026-09-23T00:02:00.000Z"}}}}},"400":{"description":"VALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"422":{"description":"SERVICE_UNAVAILABLE: Layanan tidak aktif atau tidak ditemukan. Muat ulang katalog dan pilih layanan yang tersedia.\nCOUNTRY_UNAVAILABLE: Negara tidak aktif atau tidak ditemukan. Muat ulang katalog dan pilih negara yang tersedia.\nPRICE_UNAVAILABLE: Quote/nomor tidak tersedia untuk pasangan ini. Tawarkan pasangan lain atau coba baca quote lagi nanti.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"SERVICE_UNAVAILABLE":{"summary":"Layanan tidak aktif atau tidak ditemukan.","value":{"ok":false,"error":{"code":"SERVICE_UNAVAILABLE","message":"Layanan tidak aktif atau tidak ditemukan."}}},"COUNTRY_UNAVAILABLE":{"summary":"Negara tidak aktif atau tidak ditemukan.","value":{"ok":false,"error":{"code":"COUNTRY_UNAVAILABLE","message":"Negara tidak aktif atau tidak ditemukan."}}},"PRICE_UNAVAILABLE":{"summary":"Quote/nomor tidak tersedia untuk pasangan ini.","value":{"ok":false,"error":{"code":"PRICE_UNAVAILABLE","message":"Quote/nomor tidak tersedia untuk pasangan ini."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"502":{"description":"PROVIDER_ERROR: Provider belum dapat memproses permintaan. Baca ulang status. Jangan retry pembelian upstream secara otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PROVIDER_ERROR":{"summary":"Provider belum dapat memproses permintaan.","value":{"ok":false,"error":{"code":"PROVIDER_ERROR","message":"Provider belum dapat memproses permintaan."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/orders":{"get":{"operationId":"listOrders","tags":["pesanan"],"summary":"Daftar pesanan","description":"Semua order milik pengguna, terbaru lebih dahulu. Tidak ada parameter status, limit, offset, atau cursor. Endpoint daftar tidak melakukan polling provider; gunakan GET detail untuk order terbuka.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/orders/route.ts","responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}},"example":{"ok":true,"data":[{"provider":"tiger","id":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","serviceId":"service-example","countryId":"country-example","service":{"id":"service-example","provider":"tiger","name":"WhatsApp","icon":"whatsapp","category":"Pesan","description":"Nomor untuk satu aktivasi."},"country":{"id":"country-example","provider":"tiger","name":"Indonesia","flag":"ID","dial":"+62","iso2":"ID"},"phone":"+6281200000000","price":1500,"status":"waiting","createdAt":1790121600000,"expiresAt":1790122800000,"canCancel":false,"canComplete":false,"isPreparing":false,"cancelAvailableAt":1790121720000,"message":null}]}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"operationId":"createOrder","tags":["pesanan"],"summary":"Buat pesanan","description":"Menahan saldo lalu meminta nomor ke provider. HTTP 200 berarti request ditangani, bukan jaminan nomor langsung terbit; periksa status, isPreparing, phone, dan message. Respons memakai Order, bukan activation atau token provider.\n\nMutasi finansial. Simpan clientRequestId untuk satu niat pembelian sebelum mengirim request. Timeout bukan bukti pembelian gagal. Jangan membuat UUID baru atau melakukan retry otomatis saat hasil belum pasti.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/orders/route.ts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrder"},"example":{"serviceId":"service-example","countryId":"country-example","clientRequestId":"f41c29d8-967d-469d-ae4f-0114c38bf715","priceOptionId":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expectedPriceIdr":1500}}}},"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Order"}}},"example":{"ok":true,"data":{"provider":"tiger","id":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","serviceId":"service-example","countryId":"country-example","service":{"id":"service-example","provider":"tiger","name":"WhatsApp","icon":"whatsapp","category":"Pesan","description":"Nomor untuk satu aktivasi."},"country":{"id":"country-example","provider":"tiger","name":"Indonesia","flag":"ID","dial":"+62","iso2":"ID"},"phone":"+6281200000000","price":1500,"status":"waiting","createdAt":1790121600000,"expiresAt":1790122800000,"canCancel":false,"canComplete":false,"isPreparing":false,"cancelAvailableAt":1790121720000,"message":null}}}}},"400":{"description":"BAD_REQUEST: Body JSON atau Content-Type tidak valid. Perbaiki request; untuk aksi cancel/complete, kirim {}.\nVALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"BAD_REQUEST":{"summary":"Body JSON atau Content-Type tidak valid.","value":{"ok":false,"error":{"code":"BAD_REQUEST","message":"Body JSON atau Content-Type tidak valid."}}},"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"409":{"description":"PRICE_CHANGED: Harga saat pembelian berbeda dari konfirmasi. Ambil quote baru dan minta konfirmasi harga lagi.\nPRICE_OPTION_UNAVAILABLE: Pilihan harga sudah tidak tersedia. Ambil quote baru; jangan menebak atau membuat priceOptionId.\nPURCHASE_UNRESOLVED: Masih ada pembelian yang hasilnya belum pasti. Jangan membuat pembelian baru. Tunggu rekonsiliasi.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PRICE_CHANGED":{"summary":"Harga saat pembelian berbeda dari konfirmasi.","value":{"ok":false,"error":{"code":"PRICE_CHANGED","message":"Harga saat pembelian berbeda dari konfirmasi."}}},"PRICE_OPTION_UNAVAILABLE":{"summary":"Pilihan harga sudah tidak tersedia.","value":{"ok":false,"error":{"code":"PRICE_OPTION_UNAVAILABLE","message":"Pilihan harga sudah tidak tersedia."}}},"PURCHASE_UNRESOLVED":{"summary":"Masih ada pembelian yang hasilnya belum pasti.","value":{"ok":false,"error":{"code":"PURCHASE_UNRESOLVED","message":"Masih ada pembelian yang hasilnya belum pasti."}}}}}}},"422":{"description":"INSUFFICIENT_BALANCE: Saldo tersedia tidak cukup. Tampilkan available dari server dan tawarkan top-up.\nSERVICE_UNAVAILABLE: Layanan tidak aktif atau tidak ditemukan. Muat ulang katalog dan pilih layanan yang tersedia.\nCOUNTRY_UNAVAILABLE: Negara tidak aktif atau tidak ditemukan. Muat ulang katalog dan pilih negara yang tersedia.\nPRICE_UNAVAILABLE: Quote/nomor tidak tersedia untuk pasangan ini. Tawarkan pasangan lain atau coba baca quote lagi nanti.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INSUFFICIENT_BALANCE":{"summary":"Saldo tersedia tidak cukup.","value":{"ok":false,"error":{"code":"INSUFFICIENT_BALANCE","message":"Saldo tersedia tidak cukup."}}},"SERVICE_UNAVAILABLE":{"summary":"Layanan tidak aktif atau tidak ditemukan.","value":{"ok":false,"error":{"code":"SERVICE_UNAVAILABLE","message":"Layanan tidak aktif atau tidak ditemukan."}}},"COUNTRY_UNAVAILABLE":{"summary":"Negara tidak aktif atau tidak ditemukan.","value":{"ok":false,"error":{"code":"COUNTRY_UNAVAILABLE","message":"Negara tidak aktif atau tidak ditemukan."}}},"PRICE_UNAVAILABLE":{"summary":"Quote/nomor tidak tersedia untuk pasangan ini.","value":{"ok":false,"error":{"code":"PRICE_UNAVAILABLE","message":"Quote/nomor tidak tersedia untuk pasangan ini."}}}}}}},"429":{"description":"ACTIVE_ORDER_LIMIT: Batas order aktif tercapai. Selesaikan order yang ada; limit mengikuti pengaturan operator.\nORDER_RATE_LIMIT: Terlalu banyak percobaan pembelian. Jeda percobaan. Jangan membuat loop POST.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"ACTIVE_ORDER_LIMIT":{"summary":"Batas order aktif tercapai.","value":{"ok":false,"error":{"code":"ACTIVE_ORDER_LIMIT","message":"Batas order aktif tercapai."}}},"ORDER_RATE_LIMIT":{"summary":"Terlalu banyak percobaan pembelian.","value":{"ok":false,"error":{"code":"ORDER_RATE_LIMIT","message":"Terlalu banyak percobaan pembelian."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"502":{"description":"PROVIDER_ERROR: Provider belum dapat memproses permintaan. Baca ulang status. Jangan retry pembelian upstream secara otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PROVIDER_ERROR":{"summary":"Provider belum dapat memproses permintaan.","value":{"ok":false,"error":{"code":"PROVIDER_ERROR","message":"Provider belum dapat memproses permintaan."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/orders/{id}":{"get":{"operationId":"getOrder","tags":["pesanan"],"summary":"Baca pesanan & SMS","description":"Membaca order milik sesi. Untuk order terbuka atau yang memerlukan rekonsiliasi, server dapat memeriksa provider sebelum mengembalikan DTO. Polling UI berjalan setiap 5 detik selama status waiting; status waiting juga mencakup kode yang sudah diterima.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/orders/[id]/route.ts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":128,"description":"ID order milik pengguna bersesi."},"example":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","description":"ID order milik pengguna bersesi."}],"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Order"}}},"example":{"ok":true,"data":{"provider":"tiger","id":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","serviceId":"service-example","countryId":"country-example","service":{"id":"service-example","provider":"tiger","name":"WhatsApp","icon":"whatsapp","category":"Pesan","description":"Nomor untuk satu aktivasi."},"country":{"id":"country-example","provider":"tiger","name":"Indonesia","flag":"ID","dial":"+62","iso2":"ID"},"phone":"+6281200000000","price":1500,"status":"waiting","createdAt":1790121600000,"expiresAt":1790122800000,"canCancel":false,"canComplete":true,"isPreparing":false,"cancelAvailableAt":null,"message":null,"code":"123456"}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"404":{"description":"NOT_FOUND: Resource tidak ditemukan atau bukan milik sesi. Periksa ID dan akun; ID resource tidak memberi akses lintas pengguna.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"NOT_FOUND":{"summary":"Resource tidak ditemukan atau bukan milik sesi.","value":{"ok":false,"error":{"code":"NOT_FOUND","message":"Resource tidak ditemukan atau bukan milik sesi."}}}}}}},"409":{"description":"RECONCILIATION_REQUIRED: Hasil provider perlu dikonfirmasi. Pertahankan hold; baca ulang status. Webhook Tiger dapat memakai HTTP 503 untuk kode ini.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"RECONCILIATION_REQUIRED":{"summary":"Hasil provider perlu dikonfirmasi.","value":{"ok":false,"error":{"code":"RECONCILIATION_REQUIRED","message":"Hasil provider perlu dikonfirmasi."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"502":{"description":"PROVIDER_ERROR: Provider belum dapat memproses permintaan. Baca ulang status. Jangan retry pembelian upstream secara otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PROVIDER_ERROR":{"summary":"Provider belum dapat memproses permintaan.","value":{"ok":false,"error":{"code":"PROVIDER_ERROR","message":"Provider belum dapat memproses permintaan."}}}}}}},"503":{"description":"PROVIDER_UNAVAILABLE: Provider tidak dapat dihubungi atau belum disiapkan. Periksa konfigurasi server; lanjutkan pembacaan dengan backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PROVIDER_UNAVAILABLE":{"summary":"Provider tidak dapat dihubungi atau belum disiapkan.","value":{"ok":false,"error":{"code":"PROVIDER_UNAVAILABLE","message":"Provider tidak dapat dihubungi atau belum disiapkan."}}}}}}},"504":{"description":"PROVIDER_TIMEOUT: Respons provider melewati batas waktu. Hasil bisa ambigu. Pertahankan UUID dan hold, tunggu rekonsiliasi.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PROVIDER_TIMEOUT":{"summary":"Respons provider melewati batas waktu.","value":{"ok":false,"error":{"code":"PROVIDER_TIMEOUT","message":"Respons provider melewati batas waktu."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/orders/{id}/cancel":{"post":{"operationId":"cancelOrder","tags":["pesanan"],"summary":"Batalkan pesanan","description":"Pembatalan mengikuti canCancel, cooldown server, dan status provider. Kirim JSON {}. Dana dilepas setelah hasil pembatalan dikonfirmasi, bukan hanya karena tombol ditekan atau timer lokal selesai.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/orders/[id]/cancel/route.ts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":128,"description":"ID order milik pengguna bersesi."},"example":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","description":"ID order milik pengguna bersesi."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmptyAction"},"example":{}}}},"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Order"}}},"example":{"ok":true,"data":{"provider":"tiger","id":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","serviceId":"service-example","countryId":"country-example","service":{"id":"service-example","provider":"tiger","name":"WhatsApp","icon":"whatsapp","category":"Pesan","description":"Nomor untuk satu aktivasi."},"country":{"id":"country-example","provider":"tiger","name":"Indonesia","flag":"ID","dial":"+62","iso2":"ID"},"phone":"+6281200000000","price":1500,"status":"cancelled","createdAt":1790121600000,"expiresAt":1790122800000,"canCancel":false,"canComplete":false,"isPreparing":false,"cancelAvailableAt":null,"message":null}}}}},"400":{"description":"BAD_REQUEST: Body JSON atau Content-Type tidak valid. Perbaiki request; untuk aksi cancel/complete, kirim {}.\nVALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"BAD_REQUEST":{"summary":"Body JSON atau Content-Type tidak valid.","value":{"ok":false,"error":{"code":"BAD_REQUEST","message":"Body JSON atau Content-Type tidak valid."}}},"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"404":{"description":"NOT_FOUND: Resource tidak ditemukan atau bukan milik sesi. Periksa ID dan akun; ID resource tidak memberi akses lintas pengguna.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"NOT_FOUND":{"summary":"Resource tidak ditemukan atau bukan milik sesi.","value":{"ok":false,"error":{"code":"NOT_FOUND","message":"Resource tidak ditemukan atau bukan milik sesi."}}}}}}},"409":{"description":"EARLY_CANCEL_DENIED: Belum melewati batas pembatalan. Baca ulang order dan ikuti canCancel serta cancelAvailableAt.\nINVALID_STATE: Aksi tidak cocok dengan status terkini. GET detail order sebelum menawarkan aksi kembali.\nORDER_ALREADY_TERMINAL: Order telah berakhir. Tampilkan hasil terkini dan perbarui saldo.\nRECONCILIATION_REQUIRED: Hasil provider perlu dikonfirmasi. Pertahankan hold; baca ulang status. Webhook Tiger dapat memakai HTTP 503 untuk kode ini.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"EARLY_CANCEL_DENIED":{"summary":"Belum melewati batas pembatalan.","value":{"ok":false,"error":{"code":"EARLY_CANCEL_DENIED","message":"Belum melewati batas pembatalan."}}},"INVALID_STATE":{"summary":"Aksi tidak cocok dengan status terkini.","value":{"ok":false,"error":{"code":"INVALID_STATE","message":"Aksi tidak cocok dengan status terkini."}}},"ORDER_ALREADY_TERMINAL":{"summary":"Order telah berakhir.","value":{"ok":false,"error":{"code":"ORDER_ALREADY_TERMINAL","message":"Order telah berakhir."}}},"RECONCILIATION_REQUIRED":{"summary":"Hasil provider perlu dikonfirmasi.","value":{"ok":false,"error":{"code":"RECONCILIATION_REQUIRED","message":"Hasil provider perlu dikonfirmasi."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"504":{"description":"PROVIDER_TIMEOUT: Respons provider melewati batas waktu. Hasil bisa ambigu. Pertahankan UUID dan hold, tunggu rekonsiliasi.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PROVIDER_TIMEOUT":{"summary":"Respons provider melewati batas waktu.","value":{"ok":false,"error":{"code":"PROVIDER_TIMEOUT","message":"Respons provider melewati batas waktu."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/orders/{id}/complete":{"post":{"operationId":"completeOrder","tags":["pesanan"],"summary":"Selesaikan pesanan","description":"Menyelesaikan order setelah kode diterima; ikuti canComplete dari server. Kirim JSON {}. Settlement menangkap dana yang ditahan sekali saja. Sesudahnya, revalidasi order, akun, dan ledger.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/orders/[id]/complete/route.ts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":128,"description":"ID order milik pengguna bersesi."},"example":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","description":"ID order milik pengguna bersesi."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmptyAction"},"example":{}}}},"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Order"}}},"example":{"ok":true,"data":{"provider":"tiger","id":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","serviceId":"service-example","countryId":"country-example","service":{"id":"service-example","provider":"tiger","name":"WhatsApp","icon":"whatsapp","category":"Pesan","description":"Nomor untuk satu aktivasi."},"country":{"id":"country-example","provider":"tiger","name":"Indonesia","flag":"ID","dial":"+62","iso2":"ID"},"phone":"+6281200000000","price":1500,"status":"completed","createdAt":1790121600000,"expiresAt":1790122800000,"canCancel":false,"canComplete":false,"isPreparing":false,"cancelAvailableAt":null,"message":null,"code":"123456"}}}}},"400":{"description":"BAD_REQUEST: Body JSON atau Content-Type tidak valid. Perbaiki request; untuk aksi cancel/complete, kirim {}.\nVALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"BAD_REQUEST":{"summary":"Body JSON atau Content-Type tidak valid.","value":{"ok":false,"error":{"code":"BAD_REQUEST","message":"Body JSON atau Content-Type tidak valid."}}},"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"404":{"description":"NOT_FOUND: Resource tidak ditemukan atau bukan milik sesi. Periksa ID dan akun; ID resource tidak memberi akses lintas pengguna.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"NOT_FOUND":{"summary":"Resource tidak ditemukan atau bukan milik sesi.","value":{"ok":false,"error":{"code":"NOT_FOUND","message":"Resource tidak ditemukan atau bukan milik sesi."}}}}}}},"409":{"description":"INVALID_STATE: Aksi tidak cocok dengan status terkini. GET detail order sebelum menawarkan aksi kembali.\nORDER_ALREADY_TERMINAL: Order telah berakhir. Tampilkan hasil terkini dan perbarui saldo.\nRECONCILIATION_REQUIRED: Hasil provider perlu dikonfirmasi. Pertahankan hold; baca ulang status. Webhook Tiger dapat memakai HTTP 503 untuk kode ini.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INVALID_STATE":{"summary":"Aksi tidak cocok dengan status terkini.","value":{"ok":false,"error":{"code":"INVALID_STATE","message":"Aksi tidak cocok dengan status terkini."}}},"ORDER_ALREADY_TERMINAL":{"summary":"Order telah berakhir.","value":{"ok":false,"error":{"code":"ORDER_ALREADY_TERMINAL","message":"Order telah berakhir."}}},"RECONCILIATION_REQUIRED":{"summary":"Hasil provider perlu dikonfirmasi.","value":{"ok":false,"error":{"code":"RECONCILIATION_REQUIRED","message":"Hasil provider perlu dikonfirmasi."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wallet":{"get":{"operationId":"getWallet","tags":["dompet"],"summary":"Baca saldo","description":"Saldo tercatat, dana tertahan, dan saldo tersedia dalam integer IDR. Gunakan nilai server untuk memutuskan apakah pengguna dapat membeli nomor.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/wallet/route.ts","responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Wallet"}}},"example":{"ok":true,"data":{"balance":50000,"held":1500,"available":48500}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wallet/ledger":{"get":{"operationId":"listLedger","tags":["dompet"],"summary":"Riwayat ledger","description":"Entri ledger milik pengguna, terbaru lebih dahulu, dengan pagination offset. Respons berisi baris ledger asli, bukan Transaction dari mapper tampilan. Semua query dibatasi userId sesi di SQL.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/wallet/ledger/route.ts","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50,"description":"Ukuran halaman 1–100; default 50."},"example":50,"description":"Ukuran halaman 1–100; default 50."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"description":"Offset mulai 0. Lanjutkan offset + limit saat hasMore true."},"example":0,"description":"Offset mulai 0. Lanjutkan offset + limit saat hasMore true."},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["DEPOSIT","HOLD","CAPTURE","RELEASE","REFUND","ADJUSTMENT"],"description":"Filter satu jenis ledger. Hilangkan parameter untuk semua jenis."},"example":"HOLD","description":"Filter satu jenis ledger. Hilangkan parameter untuk semua jenis."}],"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/LedgerPage"}}},"example":{"ok":true,"data":{"entries":[{"id":"67b2b171-10ac-4a0d-b5fc-714eabc8d713","userId":"user-example","type":"HOLD","amount":1500,"referenceType":"order","referenceId":"a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19","idempotencyKey":"order:a4c4b37c-9b68-4b18-bd52-8e0ea3b08d19:hold","note":null,"createdAt":"2026-09-23T00:00:00.000Z"}],"wallet":{"balance":50000,"held":1500,"available":48500},"pagination":{"limit":50,"offset":0,"hasMore":false}}}}}},"400":{"description":"VALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/payments/klikqris":{"get":{"operationId":"getActivePayment","tags":["pembayaran"],"summary":"Baca pembayaran aktif","description":"Membaca pembayaran CREATING/PENDING terakhir dari database. Bentuk data adalah { payment: Payment | null }, bukan Payment langsung. Gunakan endpoint detail untuk pemeriksaan provider.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/payments/klikqris/route.ts","responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/ActivePayment"}}},"example":{"ok":true,"data":{"payment":{"id":"5d81627b-4c2b-4cab-bd07-2a5ed675fe5f","requestedAmount":10000,"totalAmount":null,"status":"CREATING","qrisUrl":null,"qrisImage":null,"paymentUrl":null,"expiresAt":null,"createdAt":"2026-09-23T00:00:00.000Z","paidAt":null,"message":"Pembuatan QRIS sedang dikonfirmasi. Jangan buat pembayaran lain. Status akan diperiksa otomatis."}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"operationId":"createPayment","tags":["pembayaran"],"summary":"Buat top-up QRIS","description":"Membuat top-up idempotent. Satu pembayaran belum selesai per akun. Respons Payment bisa masih CREATING dengan materi QR null. Pembayaran aktif dengan nominal sama dapat dikembalikan alih-alih membuat transaksi kedua.\n\nMenjalankan request ini membuat transaksi pembayaran nyata pada instalasi yang terhubung provider. Respons hilang tidak boleh memicu pembuatan transaksi baru; pulihkan melalui GET pembayaran aktif/detail.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/payments/klikqris/route.ts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePayment"},"example":{"amount":10000,"clientRequestId":"f41c29d8-967d-469d-ae4f-0114c38bf715"}}}},"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Payment"}}},"example":{"ok":true,"data":{"id":"5d81627b-4c2b-4cab-bd07-2a5ed675fe5f","requestedAmount":10000,"totalAmount":null,"status":"CREATING","qrisUrl":null,"qrisImage":null,"paymentUrl":null,"expiresAt":null,"createdAt":"2026-09-23T00:00:00.000Z","paidAt":null,"message":"Pembuatan QRIS sedang dikonfirmasi. Jangan buat pembayaran lain. Status akan diperiksa otomatis."}}}}},"400":{"description":"BAD_REQUEST: Body JSON atau Content-Type tidak valid. Perbaiki request; untuk aksi cancel/complete, kirim {}.\nVALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"BAD_REQUEST":{"summary":"Body JSON atau Content-Type tidak valid.","value":{"ok":false,"error":{"code":"BAD_REQUEST","message":"Body JSON atau Content-Type tidak valid."}}},"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"409":{"description":"IDEMPOTENCY_CONFLICT: UUID top-up sudah dipakai untuk nominal lain. Pulihkan pembayaran awal. UUID baru hanya untuk niat transaksi baru.\nCONFLICT: Ada pembayaran aktif atau detail provider tidak cocok. Baca pembayaran aktif, jangan terus membuat top-up baru.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"IDEMPOTENCY_CONFLICT":{"summary":"UUID top-up sudah dipakai untuk nominal lain.","value":{"ok":false,"error":{"code":"IDEMPOTENCY_CONFLICT","message":"UUID top-up sudah dipakai untuk nominal lain."}}},"CONFLICT":{"summary":"Ada pembayaran aktif atau detail provider tidak cocok.","value":{"ok":false,"error":{"code":"CONFLICT","message":"Ada pembayaran aktif atau detail provider tidak cocok."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"502":{"description":"PAYMENT_PROVIDER_ERROR: Gangguan penyedia pembayaran. Baca pembayaran aktif atau detail dengan ID yang sama.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PAYMENT_PROVIDER_ERROR":{"summary":"Gangguan penyedia pembayaran.","value":{"ok":false,"error":{"code":"PAYMENT_PROVIDER_ERROR","message":"Gangguan penyedia pembayaran."}}}}}}},"503":{"description":"PAYMENT_NOT_CONFIGURED: Pembayaran belum dikonfigurasi. Pemilik instalasi harus menyiapkan KlikQRIS; bukan mengganti data dengan demo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PAYMENT_NOT_CONFIGURED":{"summary":"Pembayaran belum dikonfigurasi.","value":{"ok":false,"error":{"code":"PAYMENT_NOT_CONFIGURED","message":"Pembayaran belum dikonfigurasi."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/payments/klikqris/{id}":{"get":{"operationId":"getPayment","tags":["pembayaran"],"summary":"Periksa pembayaran","description":"Membaca pembayaran milik sesi dan melakukan poll-through terbatas untuk pembayaran aktif. UI polling 5 detik, sedangkan klaim pemeriksaan provider dibatasi sekitar 10 detik. Respons langsung Payment, bukan { payment }.\n\nSemua request memakai sesi pengguna pada origin instalasi yang sama. Mutasi harus JSON dari origin tepercaya; tidak ada autentikasi API key pelanggan.","x-source-file":"app/api/payments/klikqris/[id]/route.ts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"UUID pembayaran milik pengguna bersesi."},"example":"5d81627b-4c2b-4cab-bd07-2a5ed675fe5f","description":"UUID pembayaran milik pengguna bersesi."}],"responses":{"200":{"description":"Request ditangani. Nilai contoh bersifat ilustratif, bukan data atau jaminan hasil transaksi nyata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"data":{"$ref":"#/components/schemas/Payment"}}},"example":{"ok":true,"data":{"id":"5d81627b-4c2b-4cab-bd07-2a5ed675fe5f","requestedAmount":10000,"totalAmount":10000,"status":"PAID","qrisUrl":null,"qrisImage":null,"paymentUrl":null,"expiresAt":"2026-09-23 07:30:00","createdAt":"2026-09-23T00:00:00.000Z","paidAt":"2026-09-23T00:05:00.000Z","message":null}}}}},"400":{"description":"VALIDATION_ERROR: Input tidak sesuai schema. Periksa tipe, field wajib, format UUID, dan field tambahan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"VALIDATION_ERROR":{"summary":"Input tidak sesuai schema.","value":{"ok":false,"error":{"code":"VALIDATION_ERROR","message":"Input tidak sesuai schema."}}}}}}},"401":{"description":"UNAUTHENTICATED: Sesi tidak tersedia atau sudah berakhir. Minta pengguna masuk kembali. Jangan mengulang mutasi otomatis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"UNAUTHENTICATED":{"summary":"Sesi tidak tersedia atau sudah berakhir.","value":{"ok":false,"error":{"code":"UNAUTHENTICATED","message":"Sesi tidak tersedia atau sudah berakhir."}}}}}}},"403":{"description":"FORBIDDEN: Origin atau izin akses ditolak. Periksa origin dan role; jangan menonaktifkan guard.\nACCOUNT_SUSPENDED: Akun ditangguhkan. Hentikan operasi dan hubungi pengelola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"FORBIDDEN":{"summary":"Origin atau izin akses ditolak.","value":{"ok":false,"error":{"code":"FORBIDDEN","message":"Origin atau izin akses ditolak."}}},"ACCOUNT_SUSPENDED":{"summary":"Akun ditangguhkan.","value":{"ok":false,"error":{"code":"ACCOUNT_SUSPENDED","message":"Akun ditangguhkan."}}}}}}},"404":{"description":"NOT_FOUND: Resource tidak ditemukan atau bukan milik sesi. Periksa ID dan akun; ID resource tidak memberi akses lintas pengguna.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"NOT_FOUND":{"summary":"Resource tidak ditemukan atau bukan milik sesi.","value":{"ok":false,"error":{"code":"NOT_FOUND","message":"Resource tidak ditemukan atau bukan milik sesi."}}}}}}},"409":{"description":"CONFLICT: Ada pembayaran aktif atau detail provider tidak cocok. Baca pembayaran aktif, jangan terus membuat top-up baru.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"CONFLICT":{"summary":"Ada pembayaran aktif atau detail provider tidak cocok.","value":{"ok":false,"error":{"code":"CONFLICT","message":"Ada pembayaran aktif atau detail provider tidak cocok."}}}}}}},"500":{"description":"INTERNAL_ERROR: Kegagalan internal yang disanitasi. Periksa status transaksi sebelum retry; jangan anggap transaksi pasti gagal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"INTERNAL_ERROR":{"summary":"Kegagalan internal yang disanitasi.","value":{"ok":false,"error":{"code":"INTERNAL_ERROR","message":"Kegagalan internal yang disanitasi."}}}}}}},"502":{"description":"PAYMENT_PROVIDER_ERROR: Gangguan penyedia pembayaran. Baca pembayaran aktif atau detail dengan ID yang sama.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"PAYMENT_PROVIDER_ERROR":{"summary":"Gangguan penyedia pembayaran.","value":{"ok":false,"error":{"code":"PAYMENT_PROVIDER_ERROR","message":"Gangguan penyedia pembayaran."}}}}}}},"default":{"description":"Error aplikasi lain yang disanitasi. Gunakan error.code; jangan bergantung pada teks message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Cookie sesi HttpOnly dari Better Auth, bukan API key yang dibuat pengguna. Jangan mengekstraknya ke JavaScript. Nama aktual mengikuti protokol/base URL konfigurasi auth."},"secureSessionCookie":{"type":"apiKey","in":"cookie","name":"__Secure-better-auth.session_token","description":"Varian cookie sesi untuk secure base URL. Alternatif sessionCookie, bukan cookie kedua yang wajib dikirim. Biarkan browser atau cookie jar mengelolanya."}},"schemas":{"Service":{"type":"object","properties":{"provider":{"type":"string","enum":["tiger","smscode"],"description":"tiger = Server 1 (Tiger SMS); smscode = Server 2 (smscode.gg). Katalog, harga, dan pesanan terikat ke server ini; tidak ada perpindahan otomatis."},"id":{"type":"string","description":"ID internal dari katalog server yang dipilih. Bukan nama layanan atau kode provider."},"name":{"type":"string","description":"Nama layanan untuk ditampilkan."},"icon":{"type":"string","description":"Kunci ikon layanan, bukan URL gambar."},"category":{"type":"string","description":"Label kategori untuk tampilan."},"description":{"type":"string"}},"required":["provider","id","name","icon","category","description"],"additionalProperties":false},"Country":{"type":"object","properties":{"provider":{"type":"string","enum":["tiger","smscode"],"description":"tiger = Server 1 (Tiger SMS); smscode = Server 2 (smscode.gg). Katalog, harga, dan pesanan terikat ke server ini; tidak ada perpindahan otomatis."},"id":{"type":"string","description":"ID internal dari katalog server yang dipilih. Bukan kode ISO atau kode provider."},"name":{"type":"string"},"flag":{"type":"string","description":"Label bendera untuk tampilan; jangan dipakai sebagai identifier."},"dial":{"type":"string","description":"Kode panggilan negara untuk tampilan."},"iso2":{"type":"string","description":"Kode negara yang tersedia pada metadata."}},"required":["provider","id","name","flag","dial","iso2"],"additionalProperties":false},"QuoteOption":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"ID pilihan harga; teruskan sebagai priceOptionId, jangan buat sendiri."},"price_idr":{"type":"integer","minimum":0,"description":"Rupiah utuh, bukan sen atau string berformat."},"qualitative_stock":{"type":"string","enum":["available","limited"],"description":"Ketersediaan kualitatif, bukan jumlah stok yang dijamin."}},"required":["id","price_idr","qualitative_stock"],"additionalProperties":false},"Quote":{"type":"object","properties":{"provider":{"type":"string","enum":["tiger","smscode"],"description":"tiger = Server 1 (Tiger SMS); smscode = Server 2 (smscode.gg). Katalog, harga, dan pesanan terikat ke server ini; tidak ada perpindahan otomatis."},"serviceId":{"type":"string"},"countryId":{"type":"string"},"price_idr":{"type":"integer","minimum":0,"description":"Harga opsi pertama. Konfirmasi memakai harga opsi yang dipilih."},"qualitative_stock":{"type":"string","enum":["available","limited"],"description":"Ketersediaan kualitatif, bukan jumlah stok yang dijamin."},"options":{"type":"array","items":{"$ref":"#/components/schemas/QuoteOption"},"description":"Pilihan harga yang tersedia saat quote diambil."},"fetched_at":{"type":"string","format":"date-time","description":"ISO 8601 dengan zona waktu."},"expires_at":{"type":"string","format":"date-time","description":"Masa berlaku cache quote; bukan reservasi harga atau nomor."}},"required":["provider","serviceId","countryId","price_idr","qualitative_stock","options","fetched_at","expires_at"],"additionalProperties":false},"Wallet":{"type":"object","properties":{"balance":{"type":"integer","minimum":0,"description":"Saldo tercatat, termasuk bagian yang sedang ditahan."},"held":{"type":"integer","minimum":0,"description":"Dana yang ditahan untuk order, belum menjadi pembebanan akhir."},"available":{"type":"integer","minimum":0,"description":"Saldo yang dapat dipakai. Gunakan nilai server, bukan perhitungan client."}},"required":["balance","held","available"],"additionalProperties":false},"Order":{"type":"object","properties":{"id":{"type":"string"},"provider":{"type":"string","enum":["tiger","smscode"],"description":"tiger = Server 1 (Tiger SMS); smscode = Server 2 (smscode.gg). Katalog, harga, dan pesanan terikat ke server ini; tidak ada perpindahan otomatis."},"countryId":{"type":"string"},"serviceId":{"type":"string"},"country":{"$ref":"#/components/schemas/Country"},"service":{"$ref":"#/components/schemas/Service"},"phone":{"type":"string","description":"Nomor, atau teks status ketika nomor belum/tidak diterbitkan. Periksa isPreparing."},"price":{"type":"integer","minimum":0,"description":"Harga IDR yang dibekukan saat order dibuat."},"status":{"type":"string","enum":["waiting","completed","cancelled","expired","failed"],"description":"Status DTO client; tidak sama dengan enum database."},"createdAt":{"type":"integer","description":"Unix timestamp dalam milidetik, bukan detik."},"expiresAt":{"type":["integer","null"],"description":"Unix timestamp dalam milidetik, bukan detik."},"code":{"type":"string","description":"Opsional; field tidak dikirim bila belum ada kode terklasifikasi. SMS dapat diterima tanpa field ini. Jangan log OTP produksi."},"smsText":{"type":"string","description":"Opsional; isi SMS, termasuk teks/tautan tanpa kode. Berbeda dari message yang berisi status aplikasi. Jangan log SMS produksi."},"canCancel":{"type":"boolean","description":"Izin pembatalan pada saat respons dibuat. Server memeriksa ulang provider ketika aksi dikirim."},"canComplete":{"type":"boolean","description":"True pada CODE_RECEIVED, termasuk SMS tanpa kode. Jangan mensyaratkan code untuk menawarkan penyelesaian."},"message":{"type":["string","null"],"description":"Pesan status/kendala yang aman untuk ditampilkan, bukan isi SMS."},"isPreparing":{"type":"boolean","description":"True saat order masih CREATED atau PURCHASING."},"cancelAvailableAt":{"type":["integer","null"],"description":"Batas pembatalan menurut server. Server 1 memakai cooldown lokal; Server 2 mengikuti kemampuan provider (0 bila sudah diizinkan). Null jika bukan ACTIVE atau waktu izin belum diketahui."}},"required":["id","provider","countryId","serviceId","country","service","phone","price","status","createdAt","expiresAt","canCancel","canComplete","message","isPreparing","cancelAvailableAt"],"additionalProperties":false},"Me":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"username":{"type":["string","null"]},"role":{"type":"string","enum":["user","admin"]}},"required":["id","email","name","username","role"],"additionalProperties":false},"balance":{"type":"integer","minimum":0,"description":"Rupiah utuh, bukan sen atau string berformat."},"held":{"type":"integer","minimum":0,"description":"Rupiah utuh, bukan sen atau string berformat."},"available":{"type":"integer","minimum":0,"description":"Rupiah utuh, bukan sen atau string berformat."}},"required":["user","balance","held","available"],"additionalProperties":false},"Profile":{"type":"object","properties":{"name":{"type":"string"},"username":{"type":["string","null"]},"email":{"type":"string"}},"required":["name","username","email"],"additionalProperties":false},"UserSettings":{"type":"object","properties":{"notifications":{"type":"boolean","description":"Notifikasi SMS; default true jika belum disimpan."},"compact":{"type":"boolean","description":"Tampilan ringkas; default false jika belum disimpan."}},"required":["notifications","compact"],"additionalProperties":false},"LedgerEntry":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string","description":"Pemilik entri; query tetap dibatasi ke pengguna bersesi."},"type":{"type":"string","enum":["DEPOSIT","HOLD","CAPTURE","RELEASE","REFUND","ADJUSTMENT"]},"amount":{"type":"integer","description":"Nominal ledger asli. HOLD/CAPTURE bukan angka negatif tampilan; ADJUSTMENT dapat bertanda."},"referenceType":{"type":["string","null"]},"referenceId":{"type":["string","null"]},"idempotencyKey":{"type":"string","description":"Kunci ledger yang dihasilkan server. Bukan header request client."},"note":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601 dengan zona waktu."}},"required":["id","userId","type","amount","referenceType","referenceId","idempotencyKey","note","createdAt"],"additionalProperties":false},"LedgerPage":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/LedgerEntry"}},"wallet":{"$ref":"#/components/schemas/Wallet"},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"hasMore":{"type":"boolean"}},"required":["limit","offset","hasMore"],"additionalProperties":false}},"required":["entries","wallet","pagination"],"additionalProperties":false},"Payment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"requestedAmount":{"type":"integer","minimum":0,"description":"Jumlah top-up yang dikreditkan jika PAID terkonfirmasi."},"totalAmount":{"type":["integer","null"],"minimum":0,"description":"Total yang harus dibayar ke provider; bisa berbeda dari requestedAmount."},"status":{"type":"string","enum":["CREATING","PENDING","PAID","EXPIRED","FAILED"]},"qrisUrl":{"type":["string","null"],"description":"Materi QR dari provider; tersedia hanya untuk pembayaran aktif."},"qrisImage":{"type":["string","null"]},"paymentUrl":{"type":["string","null"]},"expiresAt":{"type":["string","null"],"description":"Timestamp provider tanpa zona waktu, disimpan apa adanya. Bukan waktu ISO UTC."},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601 dengan zona waktu."},"paidAt":{"type":["string","null"],"format":"date-time","description":"ISO 8601 dengan zona waktu."},"message":{"type":["string","null"]}},"required":["id","requestedAmount","totalAmount","status","qrisUrl","qrisImage","paymentUrl","expiresAt","createdAt","paidAt","message"],"additionalProperties":false},"ActivePayment":{"type":"object","properties":{"payment":{"anyOf":[{"$ref":"#/components/schemas/Payment"},{"type":"null"}],"description":"Pembayaran aktif, atau null jika tidak ada."}},"required":["payment"],"additionalProperties":false},"CreateOrder":{"type":"object","properties":{"serviceId":{"type":"string","minLength":1,"maxLength":128,"description":"Gunakan Service.id dari GET /api/catalog/services."},"countryId":{"type":"string","minLength":1,"maxLength":128,"description":"Gunakan Country.id dari GET /api/catalog/countries."},"clientRequestId":{"type":"string","format":"uuid","description":"UUID per niat pembelian. Pertahankan UUID yang sama saat recovery request yang sama."},"priceOptionId":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"Quote.options[].id. Harus bersama expectedPriceIdr; direkomendasikan agar harga dikonfirmasi."},"expectedPriceIdr":{"type":"integer","minimum":1,"maximum":2147483647,"description":"Quote.options[].price_idr yang disetujui pengguna. Harus bersama priceOptionId."}},"required":["serviceId","countryId","clientRequestId"],"additionalProperties":false,"dependentRequired":{"priceOptionId":["expectedPriceIdr"],"expectedPriceIdr":["priceOptionId"]}},"CreatePayment":{"type":"object","properties":{"amount":{"type":"integer","minimum":10000,"maximum":1000000,"description":"Rupiah utuh, antara 10000 dan 1000000."},"clientRequestId":{"type":"string","format":"uuid","description":"UUID untuk satu top-up. Jangan ganti UUID karena respons jaringan hilang."}},"required":["amount","clientRequestId"],"additionalProperties":false},"UpdateProfile":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60,"description":"Nama 1–60 karakter setelah trim. Wajib bersama username."},"username":{"type":"string","pattern":"^[a-zA-Z0-9._]{3,24}$","description":"3–24 karakter: huruf, angka, titik, atau underscore; input di-trim."}},"required":["name","username"],"additionalProperties":false},"UpdateSettings":{"type":"object","properties":{"notifications":{"type":"boolean"},"compact":{"type":"boolean"}},"required":[],"additionalProperties":false,"minProperties":1,"description":"Kirim minimal satu preferensi; field lain dipertahankan."},"EmptyAction":{"type":"object","properties":{},"required":[],"additionalProperties":false,"description":"Kirim body JSON {}. Body kosong atau field tambahan ditolak."},"ApiError":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["ok","error"],"additionalProperties":false}}}}