openapi: 3.0.3 info: title: Sarana — Client / Developer API version: 1.0.0 description: | API integrasi untuk **client/developer** (merchant). Berbeda dari API internal/admin — di sini hanya endpoint yang relevan untuk integrasi pembayaran. ## Getting Started (cara pakai) 1. **Dapatkan API key.** Admin Sarana membuatkan API key untuk akun kamu (hanya untuk client dengan akses developer aktif). Formatnya `sk_live_xxxxx`. 2. **Kirim API key di setiap request** lewat header: ``` X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` 3. **Cek fee dulu** (opsional) via `POST /fees/calculate` untuk tahu biaya & jumlah bersih. 4. **Buat pembayaran** — pilih channel: Virtual Account (`/payments/va`), e-wallet (`/payments/ewallet`), QRIS (`/payments/qris`), atau invoice (`/payments/invoice`). 5. **Terima notifikasi** lewat **callback/webhook** yang kamu daftarkan — Sarana akan mem-forward status pembayaran ke URL kamu (lihat tag *Callbacks*). 6. **Cek status** kapan saja via `GET /payments/{id}/status` atau lookup `by external_id`. 7. **Tarik dana** (settlement/withdrawal) ke rekening kamu via `/client/settlements` atau `/withdrawals`. ## Format response ```json { "success": true, "message": "...", "code": 200, "data": { } } ``` servers: - url: http://localhost:8080/api/v2 description: Local Development - url: https://api.internal-go.saranatechnology.com/api/v2 description: Production security: - ApiKeyAuth: [] tags: - name: Payments description: Buat pembayaran (VA, e-wallet, QRIS, invoice) & cek status - name: Fees description: Hitung biaya & jumlah bersih sebelum transaksi - name: Transactions description: Catat & lihat transaksi - name: Withdrawals description: Penarikan dana ke rekening - name: Settlements description: Settlement/pencairan untuk client - name: Bank Channels description: Daftar channel/bank yang tersedia - name: Account description: Saldo & info akun client - name: Callbacks description: Format webhook yang dikirim Sarana ke URL kamu (referensi) - name: Auth (Portal) description: Registrasi & login akun klien untuk portal (client token) - name: API Keys (Portal) description: Kelola API key sendiri (khusus klien has_developer) components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: API key client (format `sk_live_...`). Minta ke admin Sarana. ClientToken: type: http scheme: bearer description: Token sesi klien dari `POST /client/auth/login` (untuk endpoint portal). schemas: ApiResponse: type: object properties: success: { type: boolean } message: { type: string } code: { type: integer } data: { type: object } paths: # ===================== AUTH PORTAL (rencana — lihat issue #2) ===================== /auth/register: post: summary: Registrasi akun klien (portal) tags: [Auth (Portal)] security: [] description: | Self-signup klien. Akun dibuat **pending** (`has_developer=false`) dan butuh approval admin. Klien tetap bisa login & melihat statusnya, tetapi semua aksi developer dikunci (403) sampai admin mengaktifkan akses developer. requestBody: required: true content: application/json: schema: type: object required: [nama, email, password] properties: nama: { type: string } email: { type: string } password: { type: string, format: password } responses: '201': { description: Akun dibuat } '409': { description: Email sudah terdaftar } /auth/login: post: summary: Login klien (portal) tags: [Auth (Portal)] security: [] description: | Setiap klien dengan kredensial valid bisa login, termasuk akun **pending** (`has_developer=false`). Akun pending dapat melihat statusnya via `/auth/me`, tetapi setiap endpoint aksi mengembalikan 403 sampai admin menyetujui akun. Mengembalikan client token (Bearer) untuk endpoint portal. requestBody: required: true content: application/json: schema: type: object required: [email, password] properties: email: { type: string } password: { type: string, format: password } responses: '200': { description: Token + profil klien } '401': { description: Kredensial salah } /auth/me: get: summary: Profil klien dari token tags: [Auth (Portal)] security: [{ ClientToken: [] }] responses: '200': { description: Profil klien } /auth/logout: post: summary: Logout (invalidasi token) tags: [Auth (Portal)] security: [{ ClientToken: [] }] responses: '200': { description: Logout berhasil } # ===================== API KEYS PORTAL (rencana — lihat issue #2) ===================== /api-keys: get: summary: Lihat API key sendiri (ter-mask) tags: [API Keys (Portal)] security: [{ ClientToken: [] }] responses: '200': { description: API key milik klien (1 per klien) } post: summary: Generate / regenerate API key (1 per klien, key lama dihapus) tags: [API Keys (Portal)] security: [{ ClientToken: [] }] description: Khusus klien `has_developer=true`. Full key hanya ditampilkan sekali. requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string } responses: '201': { description: API key dibuat (full key tampil sekali) } '403': { description: has_developer=false } /api-keys/{id}: delete: summary: Cabut API key sendiri tags: [API Keys (Portal)] security: [{ ClientToken: [] }] parameters: - { name: id, in: path, required: true, schema: { type: integer } } responses: '200': { description: Dicabut } # ===================== REALTIME (Centrifugo) ===================== /realtime/token: get: summary: Token koneksi Centrifugo untuk chat tiket real-time tags: [Realtime] security: [{ ClientToken: [] }] description: | Mengembalikan JWT koneksi Centrifugo (TTL 60 menit). Pakai untuk connect ke server Centrifugo, lalu subscribe ke channel `chat:ticket.{id}` milik tiket Anda untuk menerima balasan admin/klien secara live. responses: '200': { description: "{ token, expires_in }" } /realtime/subscribe: get: summary: Token subscribe Centrifugo untuk channel chat tiket (scoped + cek kepemilikan) tags: [Realtime] security: [{ ClientToken: [] }] description: | Mengembalikan token subscribe Centrifugo (TTL 60 menit) khusus untuk channel `chat:ticket.{id}`. Backend memverifikasi tiket tsb milik klien dari token — jika bukan miliknya dikembalikan 403. Channel namespace `chat` tidak boleh mengaktifkan `allow_subscribe_for_client`, sehingga token scoped inilah satu-satunya cara subscribe (mencegah klien membaca chat tiket klien lain). parameters: - { name: channel, in: query, required: true, schema: { type: string }, description: "Channel, mis. `chat:ticket.5`." } responses: '200': { description: "{ token, channel, expires_in }" } '400': { description: Channel tidak valid } '403': { description: Tiket bukan milik Anda } '404': { description: Tiket tidak ditemukan } # ===================== TICKETS (Portal, client-scoped) ===================== /tickets: get: summary: Daftar tiket milik sendiri tags: [Tickets (Portal)] security: [{ ClientToken: [] }] description: client_id diambil dari token — hanya tiket milik klien tsb yang dikembalikan. parameters: - { name: status, in: query, required: false, schema: { type: string } } responses: '200': { description: Daftar tiket } post: summary: Buat tiket (lapor issue) tags: [Tickets (Portal)] security: [{ ClientToken: [] }] requestBody: required: true content: application/json: schema: type: object required: [subject, description] properties: subject: { type: string } description: { type: string } category: { type: string, enum: [bug, feature_request, question, other] } priority: { type: string, enum: [low, medium, high] } responses: '201': { description: Tiket dibuat } /tickets/{id}: get: summary: Detail tiket + balasan tags: [Tickets (Portal)] security: [{ ClientToken: [] }] parameters: - { name: id, in: path, required: true, schema: { type: integer } } responses: '200': { description: "{ ticket, replies }" } '403': { description: Tiket bukan milik klien } /tickets/{id}/reply: post: summary: Balas tiket (is_admin=false) tags: [Tickets (Portal)] security: [{ ClientToken: [] }] description: Balasan disiarkan real-time ke channel `chat:ticket.{id}` via Centrifugo. parameters: - { name: id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [message] properties: message: { type: string } responses: '201': { description: Balasan ditambahkan } '403': { description: Tiket bukan milik klien } # ===================== PROPOSALS (Portal) ===================== /proposals: post: summary: Kirim proposal/konsultasi (klien login) tags: [Proposals (Portal)] security: [{ ClientToken: [] }] requestBody: required: true content: application/json: schema: type: object required: [nama, email, deskripsi] properties: nama: { type: string } perusahaan: { type: string } email: { type: string } whatsapp: { type: string } deskripsi: { type: string } responses: '201': { description: Proposal dikirim } # ===================== FEES ===================== /fees/rates: get: summary: Daftar tarif fee per channel tags: [Fees] responses: '200': { description: Daftar tarif } /fees/calculate: post: summary: Hitung fee untuk satu channel tags: [Fees] requestBody: required: true content: application/json: schema: type: object required: [amount, channel_code] properties: amount: { type: number } channel_code: { type: string, description: "mis. BCA, OVO, QRIS" } include_vat: { type: boolean } responses: '200': { description: Rincian fee } /fees/calculate/bulk: post: summary: Hitung fee untuk banyak channel sekaligus tags: [Fees] requestBody: required: true content: application/json: schema: type: object required: [amount, channels] properties: amount: { type: number } channels: { type: array, items: { type: string } } include_vat: { type: boolean } responses: '200': { description: Rincian fee per channel } /fees/calculate/vat: post: summary: Hitung VAT tags: [Fees] requestBody: required: true content: application/json: schema: type: object required: [amount] properties: amount: { type: number } is_inclusive: { type: boolean, description: "true bila amount sudah termasuk VAT" } responses: '200': { description: Rincian VAT } /fees/calculate/withdrawal: post: summary: Hitung biaya penarikan tags: [Fees] requestBody: required: true content: application/json: schema: type: object required: [amount, bank_code] properties: amount: { type: number } bank_code: { type: string } responses: '200': { description: Biaya penarikan } /fees/calculate/amount-to-pay: post: summary: Hitung jumlah yang harus dibayar agar merchant menerima nominal tertentu tags: [Fees] requestBody: required: true content: application/json: schema: type: object required: [net_amount, channel_code] properties: net_amount: { type: number, description: "Nominal bersih yang ingin diterima" } channel_code: { type: string } include_vat: { type: boolean } responses: '200': { description: Jumlah yang harus dibayar customer } # ===================== PAYMENTS ===================== /payments/methods: get: summary: Daftar metode pembayaran tersedia tags: [Payments] responses: '200': { description: Daftar metode } /payments/va: post: summary: Buat pembayaran Virtual Account tags: [Payments] requestBody: required: true content: application/json: schema: type: object required: [client_id, amount, bank_code, customer_name] properties: client_id: { type: integer } amount: { type: number } bank_code: { type: string, description: "BCA, BNI, BRI, MANDIRI, dll" } customer_name: { type: string } responses: '201': { description: VA dibuat (berisi nomor VA) } /payments/ewallet: post: summary: Buat pembayaran e-wallet tags: [Payments] requestBody: required: true content: application/json: schema: type: object required: [client_id, amount, channel_code] properties: client_id: { type: integer } amount: { type: number } channel_code: { type: string, description: "OVO, DANA, SHOPEEPAY, LINKAJA" } customer_phone: { type: string } success_url: { type: string } responses: '201': { description: Charge e-wallet dibuat (berisi link/deeplink bayar) } /payments/qris: post: summary: Buat pembayaran QRIS tags: [Payments] requestBody: required: true content: application/json: schema: type: object required: [client_id, amount] properties: client_id: { type: integer } amount: { type: number } responses: '201': { description: QRIS dibuat (berisi qr_string) } /payments/invoice: post: summary: Buat invoice (halaman pembayaran serbaguna) tags: [Payments] requestBody: required: true content: application/json: schema: type: object required: [client_id, amount] properties: client_id: { type: integer } amount: { type: number } responses: '201': { description: Invoice dibuat (berisi invoice_url) } /payments/{id}/status: get: summary: Cek status pembayaran tags: [Payments] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: '200': { description: Status pembayaran } '404': { description: Tidak ditemukan } # ===================== TRANSACTIONS ===================== /transactions: post: summary: Catat transaksi baru tags: [Transactions] requestBody: required: true content: application/json: schema: type: object required: [client_id, total] properties: client_id: { type: integer } transaction_id: { type: string } total: { type: number } net_payment: { type: number } transaction_log: { type: object } status: { type: string } responses: '201': { description: Transaksi dibuat } /transactions/{id}: get: summary: Lihat transaksi by ID tags: [Transactions] parameters: - { name: id, in: path, required: true, schema: { type: integer } } responses: '200': { description: Detail transaksi } '404': { description: Tidak ditemukan } /transactions/external/{externalId}: get: summary: Lihat transaksi by external_id tags: [Transactions] parameters: - { name: externalId, in: path, required: true, schema: { type: string } } responses: '200': { description: Detail transaksi } '404': { description: Tidak ditemukan } # ===================== WITHDRAWALS ===================== /withdrawals: post: summary: Ajukan penarikan dana tags: [Withdrawals] description: | client_id diambil dari API key (`X-API-Key`) — tidak perlu dikirim di body. Jika akun di-set auto-approve, penarikan langsung diproses ke Xendit; jika tidak, statusnya pending menunggu approval admin. requestBody: required: true content: application/json: schema: type: object required: [jumlah, url] properties: jumlah: { type: number } biaya_penarikan: { type: number } by: { type: string, description: "Opsional; default 'client'" } url: { type: string, description: "Callback URL untuk notifikasi status penarikan" } responses: '201': { description: Penarikan diajukan (pending atau auto-approved tergantung akun) } '400': { description: Saldo kurang / sudah ada penarikan pending } '401': { description: API key tidak valid } /withdrawals/{id}: get: summary: Lihat penarikan by ID tags: [Withdrawals] parameters: - { name: id, in: path, required: true, schema: { type: integer } } responses: '200': { description: Detail penarikan } /withdrawals/external/{externalId}: get: summary: Lihat penarikan by external_id tags: [Withdrawals] parameters: - { name: externalId, in: path, required: true, schema: { type: string } } responses: '200': { description: Detail penarikan } /withdrawals/{id}/cancel: post: summary: Batalkan penarikan (hanya bila masih pending) tags: [Withdrawals] parameters: - { name: id, in: path, required: true, schema: { type: integer } } responses: '200': { description: Penarikan dibatalkan } '400': { description: Tidak bisa dibatalkan } # ===================== SETTLEMENTS (API key) ===================== /settlements: post: summary: Buat settlement/pencairan (client_id diambil dari API key) tags: [Settlements] requestBody: required: true content: application/json: schema: type: object required: [jumlah, tujuan] properties: jumlah: { type: number } tujuan: { type: string } by: { type: string } norek: { type: string } bank: { type: string } responses: '201': { description: Settlement dibuat } '400': { description: Saldo kurang / data tidak lengkap } '401': { description: API key tidak valid } # ===================== BANK CHANNELS ===================== /bank-channels/active: get: summary: Daftar channel/bank yang aktif tags: [Bank Channels] responses: '200': { description: Daftar channel aktif } /bank-channels/code/{code}: get: summary: Detail channel by kode tags: [Bank Channels] parameters: - { name: code, in: path, required: true, schema: { type: string } } responses: '200': { description: Detail channel } '404': { description: Tidak ditemukan } # ===================== ACCOUNT ===================== /clients/{id}/balance: get: summary: Cek saldo client tags: [Account] parameters: - { name: id, in: path, required: true, schema: { type: integer } } responses: '200': { description: Saldo client } # ===================== CALLBACKS (webhook reference) ===================== /callback/payment: post: summary: "[Referensi] Format callback status pembayaran yang dikirim Sarana ke URL kamu" tags: [Callbacks] description: | Sarana mengirim POST ke callback URL kamu saat status pembayaran berubah. Endpoint ini adalah **referensi format payload** — bukan untuk kamu panggil. responses: '200': { description: Acknowledged } /callback/transaction: post: summary: "[Referensi] Callback status transaksi" tags: [Callbacks] responses: '200': { description: Acknowledged } /callback/withdrawal: post: summary: "[Referensi] Callback status penarikan" tags: [Callbacks] responses: '200': { description: Acknowledged } /callback/disbursement: post: summary: "[Referensi] Callback status disbursement/settlement" tags: [Callbacks] responses: '200': { description: Acknowledged }