openapi: 3.0.3

info:
  title: IniPay API
  description: |
    API payment gateway QRIS untuk UMKM Indonesia.

    ## Mode: Production vs Sandbox

    IniPay mendukung dua mode yang dikenali otomatis dari API Key:

    | Mode | Prefix Key | Transaksi | Saldo |
    |------|-----------|-----------|-------|
    | **Production** | `pk_live_...` | Nyata (QRIS) | Saldo riil |
    | **Sandbox** | `pk_test_...` | Simulasi | Saldo terpisah |

    Keduanya menggunakan endpoint yang sama — sistem mendeteksi mode dari key yang kamu kirim.

    ## Autentikasi
    Gunakan API Key dari dashboard **Pengaturan API**. Bisa dikirim lewat:
    - **Bearer Token** (direkomendasikan): `Authorization: Bearer {api_key}`
    - **Header**: `X-Api-Key: {api_key}`
    - **Query param**: `?api_key={api_key}`

    ## Alur Integrasi (Sandbox)
    ```
    1. Ambil Sandbox Key dari dashboard → Pengaturan API
    2. POST /invoices  →  dapat invoice_id + unique_amount
    3. POST /sandbox/invoices/{id}/pay  →  simulasi pembayaran berhasil
    4. GET  /invoices/{id}  →  status berubah ke "paid"
    5. Cek webhook kamu menerima payload invoice.paid
    6. Ganti ke Production Key saat siap go-live
    ```

    ## Cara Kerja QRIS Unik
    Setiap invoice mendapat `unique_amount` = `amount` + `unique_suffix` (1–999).
    **Pelanggan harus transfer tepat sejumlah `unique_amount`**, bukan `amount`.
    Di sandbox, simulasi pembayaran dilakukan via API — tidak perlu transfer nyata.

    ## Webhook
    Saat invoice terbayar (production maupun sandbox), IniPay POST ke `webhook_url`
    kamu. Verifikasi dengan header `X-Pay-Signature` = HMAC-SHA256(body_json, webhook_secret).
    Event yang sama juga dikirim untuk perubahan status payout (`payout.completed`, `payout.rejected`).

    ## Payout — Kirim Dana ke Pihak Ketiga
    Selain menerima pembayaran (invoice), IniPay juga bisa **mengirim dana** dari saldo kamu
    ke rekening bank/e-wallet pihak ketiga — misalnya kalau kamu membangun platform/SaaS yang
    perlu membayar user-user kamu sendiri (marketplace, komisi, reseller, dll).

    **Mode: Manual.** Payout yang kamu buat lewat API akan divalidasi & saldo langsung ditahan
    (H+1), tapi transfer fisik ke tujuan dieksekusi manual oleh tim IniPay (bukan otomatis
    real-time). Status akan berubah dari `pending` → `completed` (atau `rejected` kalau gagal
    diproses), dan kamu mendapat notifikasi lewat webhook.

    > **Akses terbatas.** Fitur payout saat ini hanya aktif untuk merchant tertentu yang sudah
    > di-whitelist oleh tim IniPay. Kalau akun kamu belum diaktifkan, endpoint akan merespons
    > `403 Forbidden`. Hubungi tim IniPay untuk mengajukan akses.

    | Aturan | Nilai |
    |---|---|
    | Saldo yang dipakai | H+1 (sama seperti settlement — transaksi hari ini belum bisa di-payout) |
    | Biaya — tujuan BRI | Rp 1.000 |
    | Biaya — tujuan bank/e-wallet lain | Rp 3.000 |
    | SLA proses manual | Mengikuti jam kerja tim IniPay |

    ## Base URL
    ```
    https://www.iniaku.my.id/api/pay/v1
    ```
  version: "1.0.0"
  contact:
    name: IniAku Support
    email: hello@iniaku.my.id
    url: https://www.iniaku.my.id

servers:
  - url: https://www.iniaku.my.id/api/pay/v1
    description: Production & Sandbox (dibedakan dari API Key)

security:
  - BearerAuth: []

tags:
  - name: Invoice
    description: Buat, cek status, dan batalkan invoice pembayaran QRIS
  - name: Balance
    description: Cek saldo dan pending settlement merchant
  - name: Payout
    description: Kirim dana dari saldo kamu ke rekening/e-wallet pihak ketiga (mode manual, saldo H+1)
  - name: Sandbox
    description: "Endpoint khusus sandbox — simulasi pembayaran untuk testing integrasi (gunakan pk_test_... key)"

paths:

  /invoices:
    post:
      tags: [Invoice]
      summary: Buat Invoice Baru
      description: |
        Membuat invoice pembayaran QRIS baru. Sistem akan menghasilkan `unique_amount`
        yang harus dibayar pelanggan agar pembayaran terdeteksi otomatis.
      operationId: createInvoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvoiceRequest'
            examples:
              minimal:
                summary: Request minimal
                value:
                  amount: 50000
              lengkap:
                summary: Request lengkap
                value:
                  amount: 150000
                  external_id: "ORDER-20240101-001"
                  description: "Pembayaran pesanan kaos 3 pcs"
                  customer_name: "Budi Santoso"
                  customer_phone: "08123456789"
                  expires_minutes: 30
      responses:
        '201':
          description: Invoice berhasil dibuat
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
              example:
                id: "inv_01j8abc123"
                external_id: "ORDER-20240101-001"
                status: "pending"
                amount: 150000
                unique_amount: 150042
                unique_suffix: 42
                description: "Pembayaran pesanan kaos 3 pcs"
                customer_name: "Budi Santoso"
                customer_phone: "08123456789"
                qris_reference: null
                expires_at: "2024-01-01T11:30:00+07:00"
                paid_at: null
                created_at: "2024-01-01T11:00:00+07:00"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /invoices/{id}:
    get:
      tags: [Invoice]
      summary: Cek Status Invoice
      description: Mengambil detail dan status terkini sebuah invoice.
      operationId: getInvoice
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
      responses:
        '200':
          description: Detail invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
              examples:
                pending:
                  summary: Invoice menunggu pembayaran
                  value:
                    id: "inv_01j8abc123"
                    external_id: "ORDER-20240101-001"
                    status: "pending"
                    amount: 150000
                    unique_amount: 150042
                    unique_suffix: 42
                    description: "Pembayaran pesanan kaos 3 pcs"
                    customer_name: "Budi Santoso"
                    customer_phone: "08123456789"
                    qris_reference: null
                    expires_at: "2024-01-01T11:30:00+07:00"
                    paid_at: null
                    created_at: "2024-01-01T11:00:00+07:00"
                paid:
                  summary: Invoice sudah terbayar
                  value:
                    id: "inv_01j8abc123"
                    external_id: "ORDER-20240101-001"
                    status: "paid"
                    amount: 150000
                    unique_amount: 150042
                    unique_suffix: 42
                    description: "Pembayaran pesanan kaos 3 pcs"
                    customer_name: "Budi Santoso"
                    customer_phone: "08123456789"
                    qris_reference: "QRIS20240101001234"
                    expires_at: "2024-01-01T11:30:00+07:00"
                    paid_at: "2024-01-01T11:12:45+07:00"
                    created_at: "2024-01-01T11:00:00+07:00"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Invoice]
      summary: Batalkan Invoice
      description: |
        Membatalkan invoice yang masih berstatus `pending`.
        Invoice yang sudah `paid` atau `expired` tidak bisa dibatalkan.
      operationId: cancelInvoice
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
      responses:
        '200':
          description: Invoice berhasil dibatalkan
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Invoice dibatalkan"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Invoice tidak ditemukan atau tidak bisa dibatalkan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "Invoice tidak ditemukan atau tidak bisa dibatalkan"

  /sandbox/invoices/{id}/pay:
    post:
      tags: [Sandbox]
      summary: Simulasi Pembayaran Invoice
      description: |
        **Hanya tersedia di sandbox** (gunakan `pk_test_...` key).

        Mensimulasikan pembayaran invoice seolah pelanggan sudah transfer.
        Status invoice berubah ke `paid`, saldo sandbox merchant bertambah,
        dan webhook dikirim ke `webhook_url` kamu (jika dikonfigurasi).

        Gunakan endpoint ini untuk menguji integrasi webhook dan alur
        konfirmasi pembayaran di sistem kamu tanpa perlu transfer nyata.
      operationId: simulatePayment
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
      responses:
        '200':
          description: Pembayaran berhasil disimulasi — invoice kini berstatus paid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
              example:
                id: "inv_01j8abc123"
                mode: "sandbox"
                external_id: "ORDER-TEST-001"
                status: "paid"
                amount: 150000
                unique_amount: 150042
                unique_suffix: 42
                description: "Test pembayaran"
                customer_name: "Test User"
                customer_phone: "08123456789"
                qris_reference: "SIM-ABCDEFGHIJKL"
                expires_at: "2024-01-01T11:30:00+07:00"
                paid_at: "2024-01-01T11:05:00+07:00"
                created_at: "2024-01-01T11:00:00+07:00"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Key yang digunakan bukan sandbox key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "Endpoint ini hanya tersedia untuk sandbox key (pk_test_...)."
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Invoice tidak bisa disimulasi (bukan status pending)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "Invoice tidak bisa disimulasi karena status-nya: paid"

  /balance:
    get:
      tags: [Balance]
      summary: Cek Saldo Merchant
      description: |
        Mengambil saldo merchant saat ini dan jumlah yang sedang menunggu proses settlement.
      operationId: getBalance
      responses:
        '200':
          description: Informasi saldo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Balance'
              example:
                balance: 4750000
                pending_settle: 1200000
        '401':
          $ref: '#/components/responses/Unauthorized'

  /payouts:
    post:
      tags: [Payout]
      summary: Buat Payout Baru
      description: |
        Mengirim dana dari saldo kamu ke rekening bank/e-wallet pihak ketiga.
        Saldo yang dipakai adalah saldo H+1 (sama seperti settlement), dikurangi payout
        lain yang masih `pending`/`processing`. Payout **tidak dieksekusi otomatis** —
        tim IniPay memproses transfer manual, lalu status berubah ke `completed` atau `rejected`.
      operationId: createPayout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePayoutRequest'
            examples:
              minimal:
                summary: Request minimal
                value:
                  amount: 50000
                  destination_bank: "BRI"
                  destination_account: "112233445566"
                  destination_holder: "Budi Santoso"
              lengkap:
                summary: Request lengkap
                value:
                  amount: 250000
                  destination_bank: "DANA"
                  destination_account: "08123456789"
                  destination_holder: "Siti Aminah"
                  external_id: "PAYOUT-USER-8821"
      responses:
        '201':
          description: Payout berhasil dibuat (menunggu diproses admin)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payout'
              example:
                id: "1"
                mode: "production"
                external_id: "PAYOUT-USER-8821"
                status: "pending"
                amount: 250000
                fee: 3000
                total_deducted: 253000
                destination_bank: "DANA"
                destination_account: "08123456789"
                destination_holder: "Siti Aminah"
                transfer_proof: null
                admin_note: null
                processed_at: null
                created_at: "2024-01-01T11:00:00+07:00"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Akun kamu belum diaktifkan untuk fitur payout (masih whitelist terbatas)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "Fitur payout belum tersedia untuk akun kamu. Hubungi tim IniPay untuk mengaktifkan."
        '422':
          description: Validasi gagal atau saldo H+1 tidak cukup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "Saldo H+1 tidak cukup untuk payout ini. Tersedia: Rp 29.000"

    get:
      tags: [Payout]
      summary: Daftar Payout
      description: Mengambil 50 payout terakhir milik merchant, terbaru lebih dulu.
      operationId: listPayouts
      responses:
        '200':
          description: Daftar payout
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Payout'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /payouts/{id}:
    get:
      tags: [Payout]
      summary: Cek Status Payout
      description: Mengambil detail dan status terkini sebuah payout.
      operationId: getPayout
      parameters:
        - name: id
          in: path
          required: true
          description: ID payout
          schema:
            type: string
          example: "1"
      responses:
        '200':
          description: Detail payout
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payout'
              examples:
                pending:
                  summary: Payout masih menunggu diproses admin
                  value:
                    id: "1"
                    mode: "production"
                    external_id: "PAYOUT-USER-8821"
                    status: "pending"
                    amount: 250000
                    fee: 3000
                    total_deducted: 253000
                    destination_bank: "DANA"
                    destination_account: "08123456789"
                    destination_holder: "Siti Aminah"
                    transfer_proof: null
                    admin_note: null
                    processed_at: null
                    created_at: "2024-01-01T11:00:00+07:00"
                completed:
                  summary: Payout sudah selesai ditransfer
                  value:
                    id: "1"
                    mode: "production"
                    external_id: "PAYOUT-USER-8821"
                    status: "completed"
                    amount: 250000
                    fee: 3000
                    total_deducted: 253000
                    destination_bank: "DANA"
                    destination_account: "08123456789"
                    destination_holder: "Siti Aminah"
                    transfer_proof: "REF987654321"
                    admin_note: null
                    processed_at: "2024-01-01T15:30:00+07:00"
                    created_at: "2024-01-01T11:00:00+07:00"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: "API Key dari halaman Pengaturan API di dashboard IniPay"
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-Api-Key
    ApiKeyQuery:
      type: apiKey
      in: query
      name: api_key

  parameters:
    InvoiceId:
      name: id
      in: path
      required: true
      description: ID invoice
      schema:
        type: string
      example: "inv_01j8abc123"

  schemas:

    CreateInvoiceRequest:
      type: object
      required:
        - amount
      properties:
        amount:
          type: integer
          description: Nominal pembayaran dalam Rupiah (IDR). Pelanggan akan membayar `unique_amount` = amount + suffix unik.
          minimum: 100
          maximum: 99999999
          example: 150000
        external_id:
          type: string
          description: ID order/transaksi dari sistem kamu untuk referensi silang. Opsional tapi sangat direkomendasikan.
          maxLength: 100
          nullable: true
          example: "ORDER-20240101-001"
        description:
          type: string
          description: Keterangan pembayaran yang ditampilkan ke pelanggan.
          maxLength: 255
          nullable: true
          example: "Pembayaran pesanan kaos 3 pcs"
        customer_name:
          type: string
          description: Nama pelanggan.
          maxLength: 100
          nullable: true
          example: "Budi Santoso"
        customer_phone:
          type: string
          description: Nomor HP pelanggan (format bebas).
          maxLength: 20
          nullable: true
          example: "08123456789"
        expires_minutes:
          type: integer
          description: Masa berlaku invoice dalam menit. Default 60 menit.
          minimum: 5
          maximum: 1440
          default: 60
          example: 30

    Invoice:
      type: object
      properties:
        id:
          type: string
          description: ID unik invoice di sistem IniPay.
          example: "inv_01j8abc123"
        mode:
          type: string
          enum: [production, sandbox]
          description: Mode invoice — ditentukan dari API Key yang digunakan saat membuat invoice.
          example: "production"
        external_id:
          type: string
          nullable: true
          description: ID dari sistem kamu (jika dikirim saat buat invoice).
          example: "ORDER-20240101-001"
        status:
          type: string
          enum: [pending, paid, cancelled, expired]
          description: |
            Status invoice:
            - `pending` — Menunggu pembayaran
            - `paid` — Sudah terbayar
            - `cancelled` — Dibatalkan oleh merchant
            - `expired` — Kedaluwarsa sebelum terbayar
          example: "pending"
        amount:
          type: integer
          description: Nominal asli yang kamu set (IDR). Ini yang masuk ke saldo kamu saat terbayar.
          example: 150000
        unique_amount:
          type: integer
          description: |
            **Nominal yang harus dibayar pelanggan** = `amount` + `unique_suffix`.
            Tampilkan nilai ini di QR code / instruksi pembayaran.
          example: 150042
        unique_suffix:
          type: integer
          description: Angka unik (1–999) yang ditambahkan ke amount untuk identifikasi otomatis.
          example: 42
        description:
          type: string
          nullable: true
          example: "Pembayaran pesanan kaos 3 pcs"
        customer_name:
          type: string
          nullable: true
          example: "Budi Santoso"
        customer_phone:
          type: string
          nullable: true
          example: "08123456789"
        qris_reference:
          type: string
          nullable: true
          description: Nomor referensi QRIS dari pihak pembayar. Diisi otomatis saat status `paid`.
          example: "QRIS20240101001234"
        expires_at:
          type: string
          format: date-time
          description: Waktu kedaluwarsa invoice (ISO 8601).
          example: "2024-01-01T11:30:00+07:00"
        paid_at:
          type: string
          format: date-time
          nullable: true
          description: Waktu pembayaran diterima. Null jika belum terbayar.
          example: "2024-01-01T11:12:45+07:00"
        created_at:
          type: string
          format: date-time
          example: "2024-01-01T11:00:00+07:00"

    Balance:
      type: object
      properties:
        mode:
          type: string
          enum: [production, sandbox]
          example: "production"
        balance:
          type: integer
          description: "Saldo tersedia (IDR). Production: saldo riil. Sandbox: saldo simulasi terpisah."
          example: 4750000
        pending_settle:
          type: integer
          description: Jumlah yang sedang dalam proses settlement (selalu 0 di sandbox).
          example: 1200000

    CreatePayoutRequest:
      type: object
      required:
        - amount
        - destination_bank
        - destination_account
        - destination_holder
      properties:
        amount:
          type: integer
          description: Nominal yang diterima tujuan (IDR). Biaya transfer dipotong terpisah dari saldo kamu.
          minimum: 10000
          maximum: 99999999
          example: 250000
        destination_bank:
          type: string
          description: Nama bank atau e-wallet tujuan. `BRI` dikenakan biaya lebih murah (Rp 1.000), selain itu Rp 3.000.
          maxLength: 50
          example: "DANA"
        destination_account:
          type: string
          description: Nomor rekening/HP tujuan.
          maxLength: 30
          example: "08123456789"
        destination_holder:
          type: string
          description: Nama pemilik rekening/e-wallet tujuan.
          maxLength: 100
          example: "Siti Aminah"
        external_id:
          type: string
          description: ID referensi dari sistem kamu (mis. ID user/transaksi internal).
          maxLength: 100
          nullable: true
          example: "PAYOUT-USER-8821"

    Payout:
      type: object
      properties:
        id:
          type: string
          description: ID unik payout di sistem IniPay.
          example: "1"
        mode:
          type: string
          enum: [production, sandbox]
          example: "production"
        external_id:
          type: string
          nullable: true
          example: "PAYOUT-USER-8821"
        status:
          type: string
          enum: [pending, processing, completed, rejected, failed]
          description: |
            Status payout:
            - `pending` — Menunggu diproses admin (saldo sudah ditahan)
            - `processing` — Sedang diproses
            - `completed` — Sudah ditransfer ke tujuan
            - `rejected` — Ditolak, saldo TIDAK terpotong
            - `failed` — Gagal diproses
          example: "pending"
        amount:
          type: integer
          description: Nominal yang diterima tujuan (IDR).
          example: 250000
        fee:
          type: integer
          description: Biaya transfer (Rp 1.000 untuk BRI, Rp 3.000 untuk lainnya).
          example: 3000
        total_deducted:
          type: integer
          description: amount + fee — total yang ditahan/dipotong dari saldo kamu.
          example: 253000
        destination_bank:
          type: string
          example: "DANA"
        destination_account:
          type: string
          example: "08123456789"
        destination_holder:
          type: string
          example: "Siti Aminah"
        transfer_proof:
          type: string
          nullable: true
          description: Nomor referensi transfer, diisi admin setelah `status` menjadi `completed`.
          example: "REF987654321"
        admin_note:
          type: string
          nullable: true
          description: Catatan admin — biasanya diisi kalau payout `rejected`/`failed`.
          example: null
        processed_at:
          type: string
          format: date-time
          nullable: true
          description: Waktu payout selesai diproses (completed/rejected). Null jika masih pending.
          example: "2024-01-01T15:30:00+07:00"
        created_at:
          type: string
          format: date-time
          example: "2024-01-01T11:00:00+07:00"

    WebhookPayload:
      type: object
      description: |
        Payload yang dikirim ke `webhook_url` kamu saat invoice terbayar atau status payout berubah.
        Verifikasi keaslian dengan membandingkan header `X-Pay-Signature`
        terhadap `HMAC-SHA256(json_body, webhook_secret)`.
      properties:
        event:
          type: string
          enum: [invoice.paid, payout.completed, payout.rejected]
          example: "invoice.paid"
        id:
          type: string
          description: ID invoice atau payout terkait.
          example: "inv_01j8abc123"
        external_id:
          type: string
          nullable: true
          example: "ORDER-20240101-001"
        amount:
          type: integer
          description: Nominal asli invoice/payout (IDR) — bukan unique_amount.
          example: 150000
        status:
          type: string
          example: "paid"
        paid_at:
          type: string
          format: date-time
          nullable: true
          description: Hanya ada di event invoice.paid.
          example: "2024-01-01T11:12:45+07:00"
        reference:
          type: string
          nullable: true
          description: Nomor referensi QRIS (invoice) atau nomor referensi transfer (payout).
          example: "QRIS20240101001234"

    Error:
      type: object
      properties:
        error:
          type: string
          example: "Pesan error"

  responses:
    Unauthorized:
      description: API Key tidak valid atau tidak ditemukan
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Unauthorized"

    NotFound:
      description: Resource tidak ditemukan
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Invoice tidak ditemukan"

    UnprocessableEntity:
      description: Validasi gagal atau terjadi error saat membuat invoice
      content:
        application/json:
          schema:
            oneOf:
              - type: object
                description: Error validasi Laravel
                properties:
                  message:
                    type: string
                  errors:
                    type: object
                    additionalProperties:
                      type: array
                      items:
                        type: string
              - $ref: '#/components/schemas/Error'
          examples:
            validasi:
              summary: Error validasi field
              value:
                message: "The amount field is required."
                errors:
                  amount: ["The amount field is required."]
            runtime:
              summary: Error saat proses
              value:
                error: "Tidak ada suffix tersedia untuk nominal ini, coba lagi."
