Lewati ke konten
ZePayDokumentasi API
API v2.6.0OpenAPI
Menu dokumentasi
Referensi API · v2.6.0

ZePay Merchant API

31 endpoint dalam 6 kelompok. Setiap request ditandatangani HMAC-SHA256 dengan kunci API merchant; data selalu dibatasi ke merchant pemilik kunci. Mulai dari konsep dasar di bawah, lalu pilih endpoint.

Base URL https://payment.envfri.com/api/v2Produksi (mode live/uji ditentukan oleh awalan kunci API)

Konsep dasar

Setiap merchant berdiri sendiri: punya kunci API, saldo, rekening terdaftar, pencairan, dan webhook sendiri. Kunci API terikat ke satu merchant, jadi semua data otomatis dibatasi ke merchant pemilik kunci — tidak ada parameter merchant_id. Panduan langkah demi langkah: Panduan integrasi.

Merchant hanya lewat API#

Merchant tidak punya login web. Semua kebutuhan merchant tersedia di API ini:

Kebutuhan Endpoint
Info merchant, tarif & aturan pencairan GET /merchant
Saldo, mutasi, ringkasan periode GET /balance, GET /balance/transactions, GET /balance/summary
Transaksi: buat, cari, cek status, batalkan, simulasi (mode uji) POST/GET /payments, GET /payments/{id}, POST /payments/{id}/cancel, POST /payments/{id}/simulate
Bank & e-money, cek & daftar rekening GET /banks, POST /bank-accounts/inquiry, /bank-accounts…
Pencairan POST /payouts/quote, POST/GET /payouts, GET /payouts/{id}, POST /payouts/{id}/cancel
Webhook & riwayat event /webhook-endpoints…, GET /events, GET /events/{id}, POST /events/{id}/resend

Konsol web ZePay khusus admin ZePay. Yang hanya bisa dilakukan admin (ajukan ke tim ZePay): membuat/merotasi kunci API, membatasi scope & IP kunci, mengubah limit & ambang persetujuan, nama pemilik rekening (KYC), meninjau rekening pending_review, menyetujui pencairan besar, pencairan gabungan, serta mengatur kanal & tarif.

Biaya#

Ada dua jenis biaya ZePay (GET /merchant → fees.description): biaya transaksi per kanal pembayaran (tarif kanal pembayaran) dan biaya transfer per pencairan (tarif transfer ke bank/e-money tujuan).

Biaya Nilai yang dipakai Estimasi ditampilkan di
Transaksi (pembayaran) Ditampilkan saat tagihan dibuat (fee, fee_bearer, net_amount); nilai final saat dibayar GET /merchant → payment_methods[].channels[]
Pencairan (transfer) Dipastikan lewat pengecekan ke bank tujuan saat pratinjau & saat pencairan dibuat, lalu ditahan bersama nominal; nilai final setelah transfer selesai GET /merchant → payout.fee, GET /balance → payout_fee, GET /banks → payout_fee, POST /bank-accounts/inquiry → payout_fee
  • Tarif di GET /merchant, GET /balance, dan GET /banks hanya estimasi untuk tampilan (payout.fee_is_estimate = true, payout_fee_source = estimate). Saat ini biaya transfer Rp2.500 per pencairan ke bank dan Rp2.000 ke e-money; bila tarif berubah, angka di API ikut berubah.
  • Pembayaran: fee = biaya transaksi kanal pembayaran. Sebelum dibayar nilainya ditetapkan saat tagihan dibuat; setelah SUCCESS nilainya final. fee_bearer mengikuti aturan kanal: merchant → net_amount = amount − fee; customer → biaya dibayar pelanggan, net_amount = amount.
  • Pencairan: biaya dipotong dari saldo di luar nominal: rekening menerima amount utuh, saldo tersedia berkurang total_debited = amount + fee. Contoh: cairkan Rp100.000 dengan biaya transfer Rp2.500 → rekening menerima Rp100.000, saldo terpotong Rp102.500.
  • POST /payouts/quote memastikan biaya dengan pengecekan ke bank tujuan untuk tujuan & nominal itu (fee_source = confirmed). Bila pengecekan sementara tidak tersedia, pratinjau memakai estimasi tarif transfer (fee_source = estimate) dan biaya dihitung ulang saat pencairan dibuat. POST /payouts selalu mengecek ulang biaya ke bank tujuan; bila bank tidak merespons, pencairan tidak dibuat (503 provider_unavailable).
  • fee pada respons POST /payouts adalah biaya yang ditahan dan menjadi batas atas. Biaya final ditetapkan setelah transfer selesai: bila lebih kecil, selisihnya kembali ke saldo tersedia (baris mutasi payout_release); bila lebih besar, selisihnya ditanggung ZePay. Merchant tidak pernah membayar lebih dari biaya yang ditahan.
  • Biaya tampil di setiap respons & webhook pencairan dan sebagai baris mutasi payout_fee di /balance/transactions. Bila pencairan gagal, ditolak, atau dibatalkan, nominal dan biaya kembali ke saldo tersedia.

Autentikasi (HMAC-SHA256)#

Kirim tiga header di setiap request:

Header Isi
X-ZePay-Key-Id zpk_live_… atau zpk_test_… (mode uji)
X-ZePay-Timestamp Unix detik saat ini; selisih maks. 300 detik dari jam server ZePay
X-ZePay-Signature hex(HMAC_SHA256(secret, timestamp + "\n" + METHOD + "\n" + path_dan_query + "\n" + hex(SHA256(body)) [+ "\n" + Idempotency-Key]))

path_dan_query persis seperti yang dikirim (termasuk prefix dan encoding), mis. /api/v2/payouts?limit=20. Body kosong (GET, DELETE, atau POST tanpa body) di-hash sebagai string kosong. Bila header Idempotency-Key dikirim, nilainya ditambahkan sebagai baris terakhir string yang ditandatangani — request yang tersadap tidak bisa dikirim ulang dengan kunci lain untuk menggandakan tagihan atau pencairan. Secret (zsk_live_… / zsk_test_…) hanya diserahkan sekali saat kunci dibuat; rotasi dilakukan admin dengan masa transisi kunci lama (langsung, 24 jam, atau 7 hari). Kunci tidak dikenal, dicabut, atau kedaluwarsa → 401 authentication_failed; tanda tangan salah → 401 signature_invalid; jam meleset → 401 timestamp_out_of_range (details.server_time = jam server).

Scope & IP#

Admin bisa membatasi kunci API ke scope tertentu dan ke daftar IP (CIDR). Kunci baru mendapat semua scope:

Scope Endpoint
— (semua kunci valid) GET /merchant, GET /banks
balance:read GET /balance, GET /balance/transactions, GET /balance/summary
payments:read GET /payments, GET /payments/{id}
payments:write POST /payments, POST /payments/{id}/cancel, POST /payments/{id}/simulate
bank_accounts:read GET /bank-accounts, GET /bank-accounts/{id}
bank_accounts:write POST /bank-accounts/inquiry, POST /bank-accounts, PATCH /bank-accounts/{id}, DELETE /bank-accounts/{id}
payouts:read GET /payouts, GET /payouts/{id}
payouts:write POST /payouts/quote, POST /payouts, POST /payouts/{id}/cancel
payouts:any_destination (izin khusus, tidak otomatis; wajib IP publik spesifik, bersama payouts:write & bank_accounts:write) POST /payouts/quote & POST /payouts dengan inquiry_id — pencairan ke rekening tanpa daftar
webhooks:read GET /webhook-endpoints, GET /webhook-endpoints/{id}, GET /events, GET /events/{id}
webhooks:write POST /webhook-endpoints, PATCH /webhook-endpoints/{id}, DELETE /webhook-endpoints/{id}, POST /webhook-endpoints/{id}/rotate-secret, POST /webhook-endpoints/{id}/test, POST /events/{id}/resend

Kunci tanpa scope yang dibutuhkan, atau request dari IP di luar daftar IP kunci → 403 permission_denied. Scope tiap operasi juga tercantum di security operasi tersebut.

Idempotensi#

Header Idempotency-Key (16–64 karakter: huruf, angka, _, ., :, -; mis. UUID) wajib untuk tujuh POST berikut — tanpa header atau format salah → 400 invalid_request: POST /payments, POST /payments/{id}/cancel, POST /bank-accounts, POST /payouts, POST /payouts/{id}/cancel, POST /webhook-endpoints, POST /webhook-endpoints/{id}/rotate-secret.

  • Kirim ulang dengan kunci dan request yang sama (metode, path+query, body) → respons pertama dikembalikan apa adanya (status sama) dengan header Idempotent-Replayed: true. Respons 4xx ikut disimpan; respons 5xx tidak disimpan sehingga request bisa diulang dengan kunci yang sama.
  • Kunci sama dengan request berbeda, atau request pertama masih diproses → 409 idempotency_conflict.
  • Kunci disimpan 24 jam per merchant (mode live & uji memakai ruang kunci yang sama — jangan pakai ulang antar-mode). Untuk POST /payouts, kunci yang sama dari kunci API yang sama tidak pernah membuat pencairan kedua, juga setelah 24 jam.
  • POST /bank-accounts/inquiry, POST /payouts/quote, POST /payments/{id}/simulate, POST /webhook-endpoints/{id}/test, dan POST /events/{id}/resend tidak memakai Idempotency-Key.

Batas request & ukuran#

  • Bawaan 60 request/menit per kunci API (jendela per menit; nilainya bisa diatur ZePay per lingkungan lewat konfigurasi API_RATE_LIMIT_PER_MINUTE). Melebihi batas → 429 rate_limited dengan header Retry-After (detik).
  • Cek rekening: maks. 30/menit dan 300 per 24 jam per merchant (data pribadi; live & uji dihitung bersama).
  • Body request berupa JSON, maks. 64 KB; lebih besar → 400 invalid_request. Field yang tidak dikenal di body ditolak (400 invalid_request, nama field disebut di pesan).

Respons & error#

Setiap respons membawa header X-Request-Id (req_…) dan Cache-Control: no-store. Error selalu berbentuk { "error": { "code", "message", "request_id", "details"? } } — sertakan request_id saat menghubungi tim ZePay. Gunakan code untuk logika program; message berbahasa Indonesia untuk developer.

HTTP Kode
400 invalid_request
401 authentication_failed, signature_invalid, timestamp_out_of_range
403 permission_denied
404 not_found, inquiry_not_found, bank_account_not_found
409 idempotency_conflict, bank_account_exists, payout_not_cancellable, payment_not_cancellable, webhook_endpoint_limit, no_webhook_endpoint
422 merchant_inactive, bank_unavailable, account_not_found, inquiry_expired, inquiry_used, bank_account_rejected, bank_account_not_verified, amount_below_minimum, amount_above_maximum, payout_limit_exceeded, insufficient_balance, payment_method_unavailable
429 rate_limited
500 internal_error
503 provider_unavailable

Path yang tidak ada → 404 not_found; metode HTTP yang tidak didukung untuk path yang ada → 400 invalid_request.

Uang & waktu#

Nominal selalu bilangan bulat rupiah (tanpa desimal). Waktu di API dalam ISO 8601 dengan zona +07:00 (WIB). Pengecualian: waktu di dalam data event (mis. paid_at) dan di deliveries[] pada GET /events/{id} ditulis server dalam ISO 8601 dengan offset lain (mis. +00:00 atau Z) — selalu parse sebagai ISO 8601 lengkap.

Saldo#

  • pending — sudah dibayar pelanggan, menunggu settlement; waktunya bergantung kanal pembayaran (QRIS & e-wallet umumnya 2–4 hari, Alfamart H+3, Indomaret H+2). Pembayaran VA langsung tersedia — VA dikreditkan secara realtime.
  • available — saldo tersedia.
  • reserved — ditahan untuk pencairan yang sedang diproses / menunggu persetujuan (nominal + biaya transfer).
  • withdrawable — nominal maksimum yang bisa dicairkan sekarang: available − payout_fee (estimasi biaya transfer), dibatasi sisa limit harian.

Rekening wajib terdaftar#

Pencairan hanya bisa ke rekening/e-money terdaftar berstatus verified milik merchant. Alurnya:

  1. GET /banks — daftar bank & e-money yang didukung, format nomor, minimum/maksimum, estimasi biaya.
  2. POST /bank-accounts/inquiry — cek nomor rekening/e-money ke bank/e-money tujuan; mengembalikan nama pemilik.
  3. POST /bank-accounts dengan inquiry_id — daftarkan hasil cek (nama pemilik diambil dari bank, bukan input). Bila nama pemilik sama dengan nama pemilik terdaftar merchant (GET /merchant → account_holder_name), status langsung verified; selain itu pending_review sampai ditinjau admin ZePay (webhook bank_account.verified/rejected).
  4. POST /payouts/quote lalu POST /payouts dengan bank_account_id.

Saat pencairan diproses, ZePay mengecek ulang rekening ke bank; bila tidak ditemukan atau nama pemilik berbeda dari yang terdaftar, pencairan gagal dan dana kembali.

Pencairan langsung tanpa daftar (izin khusus)#

Untuk merchant yang perlu mengirim dana ke rekening atas nama siapa pun (mis. pembayaran ke vendor/mitra) tanpa mendaftarkan rekening & tanpa menunggu tinjauan admin. Hanya untuk kunci API dengan scope khusus payouts:any_destination, yang wajib dibatasi IP publik spesifik server merchant (IPv4 /24–/32, IPv6 /48–/128) — kunci yang bocor tidak bisa dipakai dari luar server merchant. Scope ini tidak pernah diberikan otomatis; minta ke tim ZePay. GET /merchant → payout.any_destination menunjukkan apakah kunci yang dipakai berizin.

  1. POST /bank-accounts/inquiry — wajib: cek rekening tujuan; tampilkan account_name ke pengguna untuk dikonfirmasi.
  2. POST /payouts/quote lalu POST /payouts dengan inquiry_id (bukan bank_account_id).

Hasil cek berlaku 15 menit dan sekali pakai (satu hasil cek = satu pencairan; berikutnya 422 inquiry_used, kedaluwarsa 422 inquiry_expired — cek ulang). Rekening tujuan tidak didaftarkan dan tidak muncul di GET /bank-accounts; pencairan menampilkan destination.type = inquiry_account dengan inquiry_id. Minimum, maksimum, limit harian, dan cek ulang rekening saat diproses tetap berlaku sama; ambang persetujuan dihitung kumulatif per hari untuk pencairan langsung (total hari ini + nominal baru > payout.approval_threshold → PENDING_APPROVAL). Respons hilang → ulangi dengan Idempotency-Key yang sama: pencairan yang sama dikembalikan.

Paginasi#

Daftar memakai kursor: ?limit=20&starting_after=<id> (limit 1–100, bawaan 20). Respons berisi data, has_more, dan next_cursor (id item terakhir bila masih ada halaman berikutnya, selain itu null). Urutan terbaru dulu. Filter tanggal created_from (inklusif) dan created_to (eksklusif) memakai ISO 8601, mis. 2026-10-01T00:00:00+07:00 (tulis + sebagai %2B di query string).

Mode uji (simulator)#

Kunci zpk_test_… memakai saldo, rekening, tagihan, pencairan, webhook, dan event yang terpisah dari live, dan tidak pernah menghubungi bank atau kanal pembayaran sungguhan — semua jawabannya disimulasikan secara deterministik:

Skenario Cara
Cek rekening: nama cocok → langsung verified nomor apa pun selain akhiran di bawah (nama pemilik = account_holder_name merchant)
Cek rekening: nama berbeda (BUDI SANTOSO) → pending_review nomor berakhiran 1111
Cek rekening: tidak ditemukan → 422 account_not_found nomor berakhiran 0000
Cek rekening: bank gangguan → 503 provider_unavailable nomor berakhiran 9999
Pembayaran sukses / gagal / kedaluwarsa POST /payments/{id}/simulate dengan status SUCCESS / FAILED / EXPIRED
Settlement (tertunda → tersedia, balance.settled) otomatis ±1 menit setelah simulasi SUCCESS
Pencairan gagal (nominal + biaya kembali) amount berakhiran 13, mis. 10013
Pencairan sukses nominal lain; hasil ±10 detik setelah transfer dikirim worker

Biaya di mode uji memakai estimasi: fee pencairan = payout_fee bank di GET /banks (biaya final = biaya yang ditahan); fee tagihan = fee_fixed + ceil(amount × fee_percent / 100) dari kanal di GET /merchant. QRIS, nomor VA, tautan e-wallet, dan kode bayar retail di mode uji hanya contoh dan tidak bisa dibayar sungguhan. Tagihan tetap PENDING sampai disimulasikan atau melewati expires_at (lalu ditutup EXPIRED oleh worker).

Merchant

Profil merchant pemilik kunci API, keterangan biaya, kanal pembayaran, dan aturan pencairannya.

GET/merchant#

Info merchant, tarif & aturan pencairan

Semua kunci API valid

Profil & status merchant, keterangan biaya (fees.description), metode & kanal pembayaran aktif dengan estimasi tarif per kanal, aturan pencairan (estimasi biaya transfer, minimum, limit & sisa limit harian, ambang persetujuan), webhook aktif, dan ringkasan rekening terdaftar. Bisa dipanggil dengan kunci apa pun (tanpa scope khusus).

Respons

  • 200

    OK

    Field respons (41)
    FieldTipeKeterangan
    idwajibstring (uuid)
    codewajibstring

    Prefix invoice

    namewajibstring
    statuswajibstring

    Selain active, merchant tidak bisa membuat tagihan, cek rekening, atau mencairkan (422 merchant_inactive); data tetap bisa dibaca.

    activeinactivesuspended
    livemodewajibboolean
    account_holder_namewajibstring | null

    Nama pemilik rekening sesuai data usaha (KYC). Rekening dengan nama pemilik sama langsung terverifikasi.

    contact_emailwajibstring (email) | null
    feeswajibobject

    Keterangan biaya. Tarif per kanal di payment_methods[].channels[]; estimasi biaya transfer di payout.fee.

    fees.descriptionwajibstring
    payment_methodswajibarray<PaymentMethodInfo>

    Per metode; enabled = ada minimal satu kanal aktif, channels hanya berisi kanal aktif.

    payment_methods[].methodwajibstring
    QRISVAEWALLETRETAIL
    payment_methods[].enabledwajibboolean
    payment_methods[].channelswajibarray<PaymentChannel>
    payment_methods[].channels[].channelwajibstring

    Nilai channel untuk POST /payments — QRIS, kode bank VA (014, 002, …), DANA/SHOPEEPAY/LINKAJA/OVO, atau ALFAMART/INDOMARET.

    payment_methods[].channels[].namewajibstring
    payment_methods[].channels[].fee_fixedwajibinteger

    Estimasi bagian tetap biaya transaksi (rupiah).

    (min 0)

    payment_methods[].channels[].fee_percentwajibnumber

    Estimasi persentase biaya transaksi dari nominal.

    (min 0)

    payment_methods[].channels[].fee_paid_bywajibstring

    Penanggung biaya transaksi menurut aturan kanal (sama dengan fee_bearer di objek Payment).

    merchantcustomer
    payment_methods[].channels[].min_amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    payment_methods[].channels[].max_amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    payment_methods[].channels[].expiry_minuteswajibinteger

    Masa berlaku bawaan tagihan (menit) bila expires_in_minutes tidak diisi.

    payoutwajibPayoutRules
    payout.feewajibinteger

    Estimasi biaya transfer per pencairan: tarif transfer ke bank/e-money rekening utama, atau tarif tertinggi yang aktif bila belum ada rekening utama. Biaya pasti dicek ke bank tujuan saat pratinjau & pencairan (lihat POST /payouts/quote).

    (min 0)

    payout.fee_typewajib"per_transfer"

    Biaya dikenakan sekali per pencairan menurut tarif transfer ke bank/e-money tujuan.

    payout.fee_is_estimatewajibtrue

    fee di sini estimasi untuk tampilan; biaya pasti dari POST /payouts/quote dan POST /payouts.

    payout.fee_charged_towajib"balance"

    Dipotong dari saldo tersedia di luar nominal yang diterima rekening.

    payout.minwajibinteger

    Minimum pencairan merchant (minimum bank di GET /banks juga berlaku).

    (min 0)

    payout.daily_limitwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    payout.daily_limit_remainingwajibinteger

    Sisa limit hari ini (WIB, mulai lagi 00.00), dihitung dari nominal pencairan yang tidak gagal/dibatalkan/ditolak.

    (min 0)

    payout.approval_thresholdwajibinteger

    Di atas nilai ini pencairan menunggu persetujuan admin ZePay (PENDING_APPROVAL).

    (min 0)

    payout.any_destinationwajibboolean

    true bila kunci API yang dipakai boleh mencairkan ke rekening tanpa daftar (inquiry_id): punya scope khusus payouts:any_destination dan dibatasi daftar IP.

    settlementwajibobject
    settlement.schedulewajibstring
    webhookwajibobject | null

    Endpoint webhook aktif yang paling dulu dibuat (semua endpoint di GET /webhook-endpoints).

    webhook.urlstring (uri)
    webhook.eventsarray<string>
    bank_accountswajibobject
    bank_accounts.verifiedwajibinteger
    bank_accounts.pending_reviewwajibinteger
    bank_accounts.default_idwajibstring (uuid) | null

    Rekening utama

    created_atwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/merchant' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

live
{
  "id": "c1a7e5d2-9b3f-4e60-8d21-7f4a2b9c0e13",
  "code": "SKR",
  "name": "Toko Batik Sekar",
  "status": "active",
  "livemode": true,
  "account_holder_name": "Toko Batik Sekar",
  "contact_email": "finance@tokobatiksekar.id",
  "fees": {
    "description": "Biaya transaksi mengikuti tarif kanal pembayaran; biaya pencairan mengikuti tarif transfer ke bank/e-money tujuan. Angka pasti tampil saat tagihan dibuat dan saat pratinjau pencairan."
  },
  "payment_methods": [
    {
      "method": "QRIS",
      "enabled": true,
      "channels": [
        {
          "channel": "QRIS",
          "name": "QRIS",
          "fee_fixed": 0,
          "fee_percent": 0.7,
          "fee_paid_by": "merchant",
          "min_amount": 1000,
          "max_amount": 10000000,
          "expiry_minutes": 1440
        }
      ]
    },
    {
      "method": "VA",
      "enabled": true,
      "channels": [
        {
          "channel": "014",
          "name": "BCA Virtual Account",
          "fee_fixed": 4000,
          "fee_percent": 0,
          "fee_paid_by": "merchant",
          "min_amount": 10000,
          "max_amount": 50000000,
          "expiry_minutes": 1440
        },
        {
          "channel": "002",
          "name": "BRI Virtual Account",
          "fee_fixed": 4000,
          "fee_percent": 0,
          "fee_paid_by": "merchant",
          "min_amount": 10000,
          "max_amount": 50000000,
          "expiry_minutes": 1440
        }
      ]
    },
    {
      "method": "EWALLET",
      "enabled": true,
      "channels": [
        {
          "channel": "DANA",
          "name": "DANA",
          "fee_fixed": 0,
          "fee_percent": 1.5,
          "fee_paid_by": "merchant",
          "min_amount": 15000,
          "max_amount": 2500000,
          "expiry_minutes": 60
        }
      ]
    },
    {
      "method": "RETAIL",
      "enabled": false,
      "channels": []
    }
  ],
  "payout": {
    "fee": 2500,
    "fee_type": "per_transfer",
    "fee_is_estimate": true,
    "fee_charged_to": "balance",
    "min": 10000,
    "daily_limit": 50000000,
    "daily_limit_remaining": 49950000,
    "approval_threshold": 10000000,
    "any_destination": false
  },
  "settlement": {
    "schedule": "Saldo tertunda pindah ke tersedia setelah settlement kanal pembayaran: VA langsung tersedia; QRIS & e-wallet ±2–4 hari; Alfamart H+3, Indomaret H+2."
  },
  "webhook": {
    "url": "https://tokobatik.id/hooks/zepay",
    "events": [
      "payment.*",
      "payout.*",
      "balance.*",
      "bank_account.*"
    ]
  },
  "bank_accounts": {
    "verified": 1,
    "pending_review": 0,
    "default_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11"
  },
  "created_at": "2026-03-02T09:15:00+07:00"
}

Saldo

Saldo merchant dan mutasinya, dihitung dari ledger.

GET/balance#

Saldo merchant & nominal yang bisa dicairkan

Scope balance:read

payout_fee adalah estimasi biaya transfer (payout_fee_source = estimate); biaya pasti dicek ke bank tujuan saat POST /payouts/quote dan POST /payouts. withdrawable = min(available − payout_fee, daily_limit_remaining).

Respons

  • 200

    OK

    Field respons (12)
    FieldTipeKeterangan
    currencywajib"IDR"
    pendingwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    availablewajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    reservedwajibinteger

    Ditahan untuk pencairan yang sedang diproses / menunggu persetujuan (nominal + biaya transfer).

    (min 0)

    withdrawablewajibinteger

    Maksimum amount saat ini = min(available − payout_fee, daily_limit_remaining), minimal 0. Maksimum per bank berlaku saat pencairan.

    (min 0)

    payout_feewajibinteger

    Estimasi biaya transfer (tarif bank rekening utama, atau tarif tertinggi yang aktif bila belum ada rekening utama).

    (min 0)

    payout_fee_sourcewajib"estimate"

    payout_fee adalah estimasi tarif; nilai pasti dicek ke bank tujuan saat pencairan.

    payout_minwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    daily_limitwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    daily_limit_remainingwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    livemodewajibboolean
    as_ofwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/balance' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "currency": "IDR",
  "pending": 496000,
  "available": 195250,
  "reserved": 0,
  "withdrawable": 192750,
  "payout_fee": 2500,
  "payout_fee_source": "estimate",
  "payout_min": 10000,
  "daily_limit": 50000000,
  "daily_limit_remaining": 49950000,
  "livemode": true,
  "as_of": "2026-10-31T10:42:00+07:00"
}
GET/balance/transactions#

Mutasi saldo

Scope balance:read

Setiap perubahan saldo berasal dari jurnal ledger, terbaru dulu. Biaya transfer muncul sebagai baris tersendiri (category = payout_fee); kelebihan biaya yang dikembalikan muncul sebagai payout_release. Kursor starting_after memakai id mutasi (btx_…).

Parameter

NamaDiTipeKeterangan
limitqueryinteger

(min 1, maks 100, default 20)

starting_afterquerystring

Kursor dari next_cursor halaman sebelumnya (UUID; btx_… untuk mutasi; evt_… untuk event).

categoryquerystring

payment dana masuk (tertunda) · settlement tertunda → tersedia · payout_hold tersedia → ditahan (nominal + biaya) · payout nominal keluar ke rekening · payout_fee biaya transfer (final) · payout_release ditahan → tersedia (pencairan gagal/batal/ditolak, atau kelebihan biaya transfer dikembalikan) · opening_balance saldo awal migrasi v1 · adjustment koreksi oleh admin.

Nilai: payment, settlement, payout_hold, payout, payout_fee, payout_release, opening_balance, adjustment

created_fromquerystring (date-time)

Sejak (inklusif), ISO 8601, mis. 2026-10-01T00:00:00+07:00 (tulis + sebagai %2B).

created_toquerystring (date-time)

Sampai (eksklusif), ISO 8601.

Respons

  • 200

    OK

    Field respons (11)
    FieldTipeKeterangan
    has_morewajibboolean
    next_cursorwajibstring | null

    id item terakhir bila has_more; kirim sebagai starting_after.

    datawajibarray<BalanceTransaction>
    data[].idwajibstring

    btx_<nomor>

    data[].categorywajibstring

    payment dana masuk (tertunda) · settlement tertunda → tersedia · payout_hold tersedia → ditahan (nominal + biaya) · payout nominal keluar ke rekening · payout_fee biaya transfer (final) · payout_release ditahan → tersedia (pencairan gagal/batal/ditolak, atau kelebihan biaya transfer dikembalikan) · opening_balance saldo awal migrasi v1 · adjustment koreksi oleh admin.

    paymentsettlementpayout_holdpayoutpayout_feepayout_releaseopening_balanceadjustment
    data[].referencewajibstring | null

    Invoice atau referensi pencairan

    data[].amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    data[].net_changewajibinteger

    Perubahan total saldo merchant (+ masuk, − keluar, 0 = pindah antar-bucket)

    data[].from_bucketwajibstring | null
    pendingavailablereserved
    data[].to_bucketwajibstring | null
    pendingavailablereserved
    data[].created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/balance/transactions' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

Pembayaran QRIS Rp250.000 (biaya transaksi Rp1.750), settlement, lalu pencairan Rp100.000 (biaya transfer Rp2.500)
{
  "data": [
    {
      "id": "btx_6",
      "category": "payout_fee",
      "reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
      "amount": 2500,
      "net_change": -2500,
      "from_bucket": "reserved",
      "to_bucket": null,
      "created_at": "2026-10-31T10:44:12+07:00"
    },
    {
      "id": "btx_5",
      "category": "payout",
      "reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
      "amount": 100000,
      "net_change": -100000,
      "from_bucket": "reserved",
      "to_bucket": null,
      "created_at": "2026-10-31T10:44:12+07:00"
    },
    {
      "id": "btx_4",
      "category": "payout_hold",
      "reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
      "amount": 102500,
      "net_change": 0,
      "from_bucket": "available",
      "to_bucket": "reserved",
      "created_at": "2026-10-31T10:43:05+07:00"
    },
    {
      "id": "btx_2",
      "category": "settlement",
      "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
      "amount": 248250,
      "net_change": 0,
      "from_bucket": "pending",
      "to_bucket": "available",
      "created_at": "2026-10-31T10:40:02+07:00"
    },
    {
      "id": "btx_1",
      "category": "payment",
      "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
      "amount": 248250,
      "net_change": 248250,
      "from_bucket": null,
      "to_bucket": "pending",
      "created_at": "2026-10-30T10:31:20+07:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/balance/summary#

Ringkasan periode (rekonsiliasi)

Scope balance:read

Saldo awal & akhir, pembayaran masuk, pencairan, biaya transfer, dan penyesuaian dalam periode (maks. 93 hari). closing_balance = opening_balance + payments_net − payouts − payout_fees + adjustments.

Parameter

NamaDiTipeKeterangan
fromwajibquerystring (date-time)

Awal periode (inklusif), ISO 8601.

towajibquerystring (date-time)

Akhir periode (eksklusif), ISO 8601; harus setelah from dan maks. 93 hari.

Respons

  • 200

    OK

    Field respons (15)
    FieldTipeKeterangan
    periodwajibobject

    Parameter from & to seperti yang dikirim.

    period.fromstring
    period.tostring
    opening_balancewajibinteger

    Total saldo (tertunda + tersedia + ditahan) sebelum from

    payments_netwajibinteger

    Pembayaran sukses yang masuk saldo (setelah biaya transaksi yang ditanggung merchant)

    payoutswajibinteger

    Nominal pencairan yang terkirim

    payout_feeswajibinteger

    Biaya transfer final atas pencairan tersebut

    adjustmentswajibinteger

    Penyesuaian admin & saldo awal migrasi

    closing_balancewajibinteger

    Total saldo sebelum to

    paymentswajibobject

    Pembayaran SUCCESS dengan paid_at di periode.

    payments.countinteger
    payments.grossinteger

    Rupiah, bilangan bulat.

    (min 0)

    payments.feesinteger

    Total biaya transaksi, termasuk yang ditanggung pelanggan.

    (min 0)

    payments.netinteger

    Jumlah net_amount.

    (min 0)

    payouts_countwajibinteger

    Jumlah pencairan SUCCESS yang selesai di periode

  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/balance/summary' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "period": {
    "from": "2026-10-01T00:00:00+07:00",
    "to": "2026-11-01T00:00:00+07:00"
  },
  "opening_balance": 0,
  "payments_net": 248250,
  "payouts": 100000,
  "payout_fees": 2500,
  "adjustments": 0,
  "closing_balance": 145750,
  "payments": {
    "count": 1,
    "gross": 250000,
    "fees": 1750,
    "net": 248250
  },
  "payouts_count": 1
}

Bank & rekening

Daftar bank & e-money, cek rekening, dan rekening terdaftar milik merchant.

GET/banks#

Daftar bank & e-money tujuan pencairan

Semua kunci API valid

Bank dan e-money yang bisa dicek dan dicairkan, termasuk yang sedang gangguan (available = false), lengkap dengan minimum, maksimum, format nomor, dan estimasi biaya transfer (payout_fee_source = estimate). Biaya yang dipotong saat pencairan dipastikan lewat pengecekan ke bank tujuan. Bisa dipanggil dengan kunci apa pun.

Parameter

NamaDiTipeKeterangan
typequerystring

Saring bank saja atau e-money saja.

Nilai: bank, ewallet

Respons

  • 200

    OK

    Field respons (11)
    FieldTipeKeterangan
    datawajibarray<Bank>
    data[].codewajibstring
    data[].namewajibstring
    data[].typewajibstring
    bankewallet
    data[].availablewajibboolean

    false bila bank/e-money sedang gangguan; cek & pencairan ditolak sementara (bank_unavailable).

    data[].payout_feewajibinteger

    Estimasi biaya transfer ke bank/e-money ini (tarif saat ini Rp2.500 bank, Rp2.000 e-money). Biaya yang dipotong dicek ke bank tujuan saat pencairan.

    (min 0)

    data[].payout_fee_sourcewajib"estimate"
    data[].payout_minwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    data[].payout_maxwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    data[].account_number_patternwajibstring

    Regex format nomor

    data[].account_number_hintwajibstring | null
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/banks' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

campuran
{
  "data": [
    {
      "code": "014",
      "name": "BCA",
      "type": "bank",
      "available": true,
      "payout_fee": 2500,
      "payout_fee_source": "estimate",
      "payout_min": 10000,
      "payout_max": 50000000,
      "account_number_pattern": "^[0-9]{10}$",
      "account_number_hint": "10 digit"
    },
    {
      "code": "DANA",
      "name": "DANA",
      "type": "ewallet",
      "available": true,
      "payout_fee": 2000,
      "payout_fee_source": "estimate",
      "payout_min": 20000,
      "payout_max": 10000000,
      "account_number_pattern": "^08[0-9]{8,11}$",
      "account_number_hint": "Nomor HP terdaftar di DANA, awalan 08"
    }
  ]
}
POST/bank-accounts/inquiry#

Cek rekening atau e-money sebelum didaftarkan

Scope bank_accounts:write

ZePay mengecek nomor ke bank/e-money tujuan dan mengembalikan nama pemilik serta perkiraan biaya transfer ke tujuan itu (payout_fee). Hasilnya berlaku 15 menit dan dipakai sekali untuk POST /bank-accounts. Cek ini tidak mendaftarkan rekening dan tidak memotong saldo. Spasi, titik, dan tanda hubung di nomor diabaikan. Dibatasi 30/menit dan 300 per 24 jam per merchant. Tidak memakai Idempotency-Key.

Body JSON

FieldTipeKeterangan
bank_codewajibstring

Kode dari GET /banks

(panjang min 2, panjang maks 20)

account_numberwajibstring

Nomor rekening, atau nomor HP untuk e-money. Harus cocok dengan account_number_pattern bank.

(panjang min 5, panjang maks 30)

Respons

  • 200

    Rekening/e-money ditemukan

    Field respons (10)
    FieldTipeKeterangan
    idwajibstring (uuid)

    Kirim sebagai inquiry_id ke POST /bank-accounts.

    bank_codewajibstring
    bank_namewajibstring
    typewajibstring
    bankewallet
    account_number_maskedwajibstring
    account_namewajibstring

    Nama pemilik menurut bank/e-money

    name_matches_merchantwajibboolean

    true → akan langsung verified saat didaftarkan; false → pending_review (ditinjau admin).

    payout_feewajibinteger

    Perkiraan biaya transfer ke tujuan ini: hasil pengecekan ke bank tujuan, atau estimasi tarif transfer bila bank tidak memberikannya. Biaya yang dipotong dicek ulang ke bank tujuan saat pencairan dibuat.

    (min 0)

    expires_atwajibstring (date-time)

    Batas waktu pendaftaran (15 menit).

    livemodewajibboolean
  • 400

    invalid_request — bank_code tidak dikenal atau format nomor tidak cocok (details.account_number_pattern)

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 422

    account_not_found — nomor tidak ditemukan di bank/e-money tujuan; bank_unavailable — bank sedang tidak tersedia; merchant_inactive — merchant ditangguhkan/nonaktif.

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

  • 503

    provider_unavailable — bank atau kanal pembayaran sedang tidak merespons; coba lagi. Cek rekening: ulangi cek. Pencairan: tidak dibuat (aman diulang dengan Idempotency-Key yang sama). Tagihan: tagihan ditutup FAILED (details.payment_id, details.reference) — buat tagihan baru.

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/bank-accounts/inquiry' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Content-Type: application/json' \
  -d '{"bank_code":"014","account_number":"8730124821"}'
bank
{
  "bank_code": "014",
  "account_number": "8730124821"
}
emoney
{
  "bank_code": "DANA",
  "account_number": "081234567788"
}

Contoh respons 200

JSON
{
  "id": "3b1d6c2e-8f7a-4c11-9d0e-5a2b7c9e1f30",
  "bank_code": "014",
  "bank_name": "BCA",
  "type": "bank",
  "account_number_masked": "•••• 4821",
  "account_name": "TOKO BATIK SEKAR",
  "name_matches_merchant": true,
  "payout_fee": 2500,
  "expires_at": "2026-10-31T10:57:00+07:00",
  "livemode": true
}
GET/bank-accounts#

Rekening & e-money terdaftar milik merchant

Scope bank_accounts:read

Rekening utama di urutan pertama, lalu terbaru dulu.

Parameter

NamaDiTipeKeterangan
limitqueryinteger

(min 1, maks 100, default 20)

starting_afterquerystring

Kursor dari next_cursor halaman sebelumnya (UUID; btx_… untuk mutasi; evt_… untuk event).

typequerystring

Nilai: bank, ewallet

statusquerystring

verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

Nilai: pending_review, verified, rejected, disabled

Respons

  • 200

    OK

    Field respons (15)
    FieldTipeKeterangan
    has_morewajibboolean
    next_cursorwajibstring | null

    id item terakhir bila has_more; kirim sebagai starting_after.

    datawajibarray<BankAccount>
    data[].idwajibstring (uuid)
    data[].bank_codewajibstring
    data[].bank_namewajibstring
    data[].typewajibstring
    bankewallet
    data[].account_number_maskedwajibstring
    data[].account_namewajibstring

    Nama pemilik dari hasil cek rekening

    data[].labelwajibstring | null
    data[].statuswajibstring

    verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

    pending_reviewverifiedrejecteddisabled
    data[].is_defaultwajibboolean
    data[].review_notewajibstring | null

    Alasan bila ditolak admin

    data[].verified_atwajibstring (date-time) | null
    data[].created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/bank-accounts' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "has_more": true,
  "next_cursor": "string",
  "data": [
    {
      "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
      "bank_code": "014",
      "bank_name": "BCA",
      "type": "bank",
      "account_number_masked": "•••• 4821",
      "account_name": "TOKO BATIK SEKAR",
      "label": "Rekening operasional",
      "status": "verified",
      "is_default": true,
      "review_note": null,
      "verified_at": "2026-10-31T10:42:31+07:00",
      "created_at": "2026-10-31T10:42:31+07:00"
    }
  ]
}
POST/bank-accounts#

Daftarkan rekening dari hasil cek

Scope bank_accounts:writeWajib Idempotency-Key

Wajib memakai inquiry_id dari POST /bank-accounts/inquiry (maks. 15 menit, sekali pakai). Nama pemilik diambil dari hasil cek. Status verified bila nama sama dengan account_holder_name merchant, selain itu pending_review. Rekening terverifikasi pertama otomatis menjadi rekening utama. Rekening yang pernah dinonaktifkan bisa didaftarkan lagi dari hasil cek baru.

Body JSON

FieldTipeKeterangan
inquiry_idwajibstring (uuid)
labelstring

(panjang maks 60)

is_defaultboolean

Jadikan rekening utama (hanya berlaku bila langsung verified).

(default false)

Respons

  • 201

    Terdaftar (verified atau pending_review)

    Field respons (12)
    FieldTipeKeterangan
    idwajibstring (uuid)
    bank_codewajibstring
    bank_namewajibstring
    typewajibstring
    bankewallet
    account_number_maskedwajibstring
    account_namewajibstring

    Nama pemilik dari hasil cek rekening

    labelwajibstring | null
    statuswajibstring

    verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

    pending_reviewverifiedrejecteddisabled
    is_defaultwajibboolean
    review_notewajibstring | null

    Alasan bila ditolak admin

    verified_atwajibstring (date-time) | null
    created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    inquiry_not_found — hasil cek tidak ada, milik merchant lain, atau dari mode lain

  • 409

    bank_account_exists — rekening sudah terdaftar; atau idempotency_conflict

  • 422

    inquiry_expired, inquiry_used, account_not_found, atau bank_account_rejected

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/bank-accounts' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Idempotency-Key: 0b9f3c1e-7a52-4d0e-9f4e-2c1d8a6b3e70' \
  -H 'Content-Type: application/json' \
  -d '{"inquiry_id":"3b1d6c2e-8f7a-4c11-9d0e-5a2b7c9e1f30","label":"Rekening operasional"}'

Contoh respons 201

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "bank_code": "014",
  "bank_name": "BCA",
  "type": "bank",
  "account_number_masked": "•••• 4821",
  "account_name": "TOKO BATIK SEKAR",
  "label": "Rekening operasional",
  "status": "verified",
  "is_default": true,
  "review_note": null,
  "verified_at": "2026-10-31T10:42:31+07:00",
  "created_at": "2026-10-31T10:42:31+07:00"
}
GET/bank-accounts/{id}#

Detail rekening terdaftar

Scope bank_accounts:read

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 200

    OK

    Field respons (12)
    FieldTipeKeterangan
    idwajibstring (uuid)
    bank_codewajibstring
    bank_namewajibstring
    typewajibstring
    bankewallet
    account_number_maskedwajibstring
    account_namewajibstring

    Nama pemilik dari hasil cek rekening

    labelwajibstring | null
    statuswajibstring

    verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

    pending_reviewverifiedrejecteddisabled
    is_defaultwajibboolean
    review_notewajibstring | null

    Alasan bila ditolak admin

    verified_atwajibstring (date-time) | null
    created_atwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/bank-accounts/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "bank_code": "014",
  "bank_name": "BCA",
  "type": "bank",
  "account_number_masked": "•••• 4821",
  "account_name": "TOKO BATIK SEKAR",
  "label": "Rekening operasional",
  "status": "verified",
  "is_default": true,
  "review_note": null,
  "verified_at": "2026-10-31T10:42:31+07:00",
  "created_at": "2026-10-31T10:42:31+07:00"
}
PATCH/bank-accounts/{id}#

Ubah label atau jadikan rekening utama

Scope bank_accounts:write

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Body JSON

FieldTipeKeterangan
labelstring | null

null menghapus label.

(panjang maks 60)

is_defaulttrue

Rekening utama sebelumnya otomatis dilepas.

Respons

  • 200

    Diperbarui

    Field respons (12)
    FieldTipeKeterangan
    idwajibstring (uuid)
    bank_codewajibstring
    bank_namewajibstring
    typewajibstring
    bankewallet
    account_number_maskedwajibstring
    account_namewajibstring

    Nama pemilik dari hasil cek rekening

    labelwajibstring | null
    statuswajibstring

    verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

    pending_reviewverifiedrejecteddisabled
    is_defaultwajibboolean
    review_notewajibstring | null

    Alasan bila ditolak admin

    verified_atwajibstring (date-time) | null
    created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 422

    bank_account_not_verified — hanya rekening terverifikasi yang bisa jadi rekening utama

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X PATCH 'https://payment.envfri.com/api/v2/bank-accounts/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Content-Type: application/json' \
  -d '{"label":"string","is_default":true}'

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "bank_code": "014",
  "bank_name": "BCA",
  "type": "bank",
  "account_number_masked": "•••• 4821",
  "account_name": "TOKO BATIK SEKAR",
  "label": "Rekening operasional",
  "status": "verified",
  "is_default": true,
  "review_note": null,
  "verified_at": "2026-10-31T10:42:31+07:00",
  "created_at": "2026-10-31T10:42:31+07:00"
}
DELETE/bank-accounts/{id}#

Nonaktifkan rekening

Scope bank_accounts:write

Rekening verified/pending_review menjadi disabled dan tidak bisa lagi dipakai untuk pencairan baru (rekening rejected/disabled tidak berubah). Pencairan yang sedang berjalan tidak terpengaruh. Daftarkan ulang lewat cek rekening bila diperlukan.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 200

    Dinonaktifkan

    Field respons (12)
    FieldTipeKeterangan
    idwajibstring (uuid)
    bank_codewajibstring
    bank_namewajibstring
    typewajibstring
    bankewallet
    account_number_maskedwajibstring
    account_namewajibstring

    Nama pemilik dari hasil cek rekening

    labelwajibstring | null
    statuswajibstring

    verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

    pending_reviewverifiedrejecteddisabled
    is_defaultwajibboolean
    review_notewajibstring | null

    Alasan bila ditolak admin

    verified_atwajibstring (date-time) | null
    created_atwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X DELETE 'https://payment.envfri.com/api/v2/bank-accounts/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "bank_code": "014",
  "bank_name": "BCA",
  "type": "bank",
  "account_number_masked": "•••• 4821",
  "account_name": "TOKO BATIK SEKAR",
  "label": "Rekening operasional",
  "status": "verified",
  "is_default": true,
  "review_note": null,
  "verified_at": "2026-10-31T10:42:31+07:00",
  "created_at": "2026-10-31T10:42:31+07:00"
}

Pencairan

Pencairan dari saldo merchant ke rekening terdaftar; biaya transfer dipotong dari saldo di luar nominal.

POST/payouts/quote#

Pratinjau pencairan (biaya transfer & total dipotong)

Scope payouts:write

Menghitung nominal diterima, biaya transfer, total dipotong dari saldo, saldo setelah pencairan, dan apakah perlu persetujuan — tanpa membuat pencairan. ZePay memastikan biaya dengan pengecekan ke bank tujuan untuk tujuan & nominal ini (fee_source = confirmed); bila pengecekan sementara tidak tersedia, dipakai estimasi tarif transfer (fee_source = estimate) dan biaya dihitung ulang saat pencairan dibuat. Validasinya sama dengan POST /payouts. Tampilkan fee dan total_debited ke pengguna sebelum konfirmasi. Tidak memakai Idempotency-Key. Dengan inquiry_id (kunci berizin payouts:any_destination), pratinjau tidak memakai hasil cek rekening.

Body JSON

FieldTipeKeterangan
bank_account_idstring (uuid)

Rekening terdaftar berstatus verified.

inquiry_idstring (uuid)

Hasil POST /bank-accounts/inquiry (rekening valid, ≤ 15 menit, belum dipakai) — pencairan ke rekening tanpa daftar. Hanya untuk kunci berizin payouts:any_destination (selain itu 403 permission_denied).

amountinteger

Nominal yang DITERIMA rekening. Minimum = terbesar dari payout.min merchant dan payout_min bank (bawaan Rp10.000), maksimum = payout_max bank (422). Biaya transfer dipotong dari saldo di luar nominal ini.

(min 1, maks 1000000000)

withdraw_allboolean

Cairkan seluruh saldo yang bisa dicairkan = min(tersedia − biaya transfer, sisa limit harian, maksimum bank).

(default false)

Respons

  • 200

    Pencairan bisa dibuat dengan rincian berikut

    Field respons (19)
    FieldTipeKeterangan
    amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    feewajibinteger

    Biaya transfer untuk tujuan & nominal ini.

    (min 0)

    fee_sourcewajibstring

    confirmed = biaya dipastikan lewat pengecekan ke bank tujuan; estimate = pengecekan sementara tidak tersedia, dipakai estimasi tarif transfer — biaya dihitung ulang saat pencairan dibuat.

    confirmedestimate
    total_debitedwajibinteger

    amount + fee, dipotong dari saldo tersedia.

    (min 0)

    available_beforewajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    available_afterwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    withdrawablewajibinteger

    min(tersedia − fee, sisa limit harian, maksimum bank).

    (min 0)

    daily_limitwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    daily_limit_remainingwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    requires_approvalwajibboolean

    true bila amount di atas ambang persetujuan.

    destinationwajibPayoutDestinationMerchantAccount | PayoutDestinationInquiryAccount
    destination.type"inquiry_account"
    destination.bank_account_idnull
    destination.bank_codestring
    destination.bank_namestring
    destination.account_typestring
    bankewallet
    destination.account_namestring
    destination.account_number_maskedstring
    destination.inquiry_idstring (uuid)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    bank_account_not_found — rekening tidak terdaftar untuk merchant ini; atau inquiry_not_found — hasil cek rekening tidak ditemukan (bukan milik merchant/mode ini)

  • 422

    Pencairan tidak bisa dibuat: bank_account_not_verified, insufficient_balance (details: amount, fee, total_debited, available; tanpa amount/total_debited bila withdraw_all), payout_limit_exceeded, amount_below_minimum, amount_above_maximum, merchant_inactive, bank_unavailable, atau account_not_found (rekening tujuan tidak lagi ditemukan saat pengecekan biaya ke bank tujuan, atau hasil cek inquiry_id bukan rekening valid). Dengan inquiry_id: inquiry_expired (lewat 15 menit) atau inquiry_used (sudah dipakai) — cek ulang rekening.

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/payouts/quote' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Content-Type: application/json' \
  -d '{"bank_account_id":"6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11","amount":100000}'
nominal
{
  "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "amount": 100000
}
semua
{
  "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "withdraw_all": true
}
Tanpa daftar (izin payouts:any_destination)
{
  "inquiry_id": "3b8e1f52-7c0a-4d9e-b6a1-5f2c8d4e9a03",
  "amount": 100000
}

Contoh respons 200

JSON
{
  "amount": 100000,
  "fee": 2500,
  "fee_source": "confirmed",
  "total_debited": 102500,
  "available_before": 195250,
  "available_after": 92750,
  "withdrawable": 192750,
  "daily_limit": 50000000,
  "daily_limit_remaining": 49950000,
  "requires_approval": false,
  "destination": {
    "type": "merchant_account",
    "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
    "bank_code": "014",
    "bank_name": "BCA",
    "account_type": "bank",
    "account_name": "TOKO BATIK SEKAR",
    "account_number_masked": "•••• 4821"
  }
}
GET/payouts#

Riwayat pencairan

Scope payouts:read

Termasuk pencairan gabungan yang dibuat admin ZePay dari beberapa merchant (combined = true); yang ditampilkan hanya porsi merchant ini.

Parameter

NamaDiTipeKeterangan
limitqueryinteger

(min 1, maks 100, default 20)

starting_afterquerystring

Kursor dari next_cursor halaman sebelumnya (UUID; btx_… untuk mutasi; evt_… untuk event).

statusquerystring

PENDING_APPROVAL menunggu admin ZePay · PROCESSING sedang dikirim ke bank tujuan · SUCCESS diterima rekening · FAILED gagal · CANCELLED dibatalkan merchant · REJECTED ditolak admin.

Nilai: PENDING_APPROVAL, PROCESSING, SUCCESS, FAILED, CANCELLED, REJECTED

created_fromquerystring (date-time)

Sejak (inklusif), ISO 8601, mis. 2026-10-01T00:00:00+07:00 (tulis + sebagai %2B).

created_toquerystring (date-time)

Sampai (eksklusif), ISO 8601.

Respons

  • 200

    OK

    Field respons (24)
    FieldTipeKeterangan
    has_morewajibboolean
    next_cursorwajibstring | null

    id item terakhir bila has_more; kirim sebagai starting_after.

    datawajibarray<Payout>
    data[].idwajibstring (uuid)
    data[].referencewajibstring

    Referensi publik pencairan.

    data[].statuswajibstring

    PENDING_APPROVAL menunggu admin ZePay · PROCESSING sedang dikirim ke bank tujuan · SUCCESS diterima rekening · FAILED gagal · CANCELLED dibatalkan merchant · REJECTED ditolak admin.

    PENDING_APPROVALPROCESSINGSUCCESSFAILEDCANCELLEDREJECTED
    data[].amountwajibinteger

    Nominal (porsi merchant ini) yang dikirim ke rekening tujuan.

    (min 0)

    data[].feewajibinteger

    Biaya transfer yang dibebankan ke saldo merchant ini. Sebelum SUCCESS = biaya yang ditahan (hasil pengecekan ke bank tujuan, batas atas); setelah SUCCESS = biaya final (bila lebih kecil, selisihnya sudah dikembalikan). Pada pencairan gabungan = porsi biaya merchant ini; 0 bila ditanggung ZePay.

    (min 0)

    data[].total_debitedwajibinteger

    amount + fee. Kembali utuh ke saldo tersedia bila FAILED/CANCELLED/REJECTED.

    (min 0)

    data[].combinedwajibboolean

    true bila pencairan gabungan dari beberapa merchant yang dibuat admin.

    data[].destinationwajibPayoutDestinationMerchantAccount | PayoutDestinationInquiryAccount | PayoutDestinationZepayAccount
    data[].destination.type"zepay_account"
    data[].destination.bank_account_idnull
    data[].destination.bank_codestring
    data[].destination.bank_namestring
    data[].destination.account_typestring
    bankewallet
    data[].destination.account_namestring
    data[].destination.account_number_maskedstring
    data[].destination.inquiry_idstring (uuid)
    data[].notewajibstring | null
    data[].failure_reasonwajibstring | null

    Alasan bila FAILED / CANCELLED / REJECTED.

    data[].livemodewajibboolean
    data[].created_atwajibstring (date-time)
    data[].completed_atwajibstring (date-time) | null
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/payouts' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "has_more": true,
  "next_cursor": "string",
  "data": [
    {
      "id": "9d0f3c1a-2b7e-4f55-8a19-6c3e2d1b0a77",
      "reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
      "status": "PROCESSING",
      "amount": 100000,
      "fee": 2500,
      "total_debited": 102500,
      "combined": false,
      "destination": {
        "type": "merchant_account",
        "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
        "bank_code": "014",
        "bank_name": "BCA",
        "account_type": "bank",
        "account_name": "TOKO BATIK SEKAR",
        "account_number_masked": "•••• 4821"
      },
      "note": "Tarik mingguan",
      "failure_reason": null,
      "livemode": true,
      "created_at": "2026-10-31T10:43:05+07:00",
      "completed_at": null
    }
  ]
}
POST/payouts#

Cairkan saldo ke rekening terdaftar (atau hasil cek rekening)

Scope payouts:writeWajib Idempotency-Key

Hanya dari saldo merchant pemilik kunci, ke rekening terdaftar berstatus verified (bank_account_id) — atau, khusus kunci berizin payouts:any_destination yang dibatasi IP, langsung ke hasil cek rekening (inquiry_id, berlaku 15 menit, sekali pakai; lihat "Pencairan langsung tanpa daftar"). ZePay mengecek biaya transfer ke bank tujuan — bila bank tidak merespons, pencairan tidak dibuat (503 provider_unavailable, aman diulang dengan Idempotency-Key yang sama). Biaya transfer dipotong dari saldo di luar amount; nominal + biaya langsung ditahan. Biaya final ditetapkan setelah transfer selesai: bila lebih kecil, selisihnya dikembalikan; bila lebih besar, ditanggung ZePay. Di atas payout.approval_threshold status PENDING_APPROVAL sampai disetujui admin ZePay.

Body JSON

FieldTipeKeterangan
bank_account_idstring (uuid)

Rekening terdaftar berstatus verified.

inquiry_idstring (uuid)

Hasil POST /bank-accounts/inquiry (rekening valid, ≤ 15 menit, belum dipakai) — pencairan langsung ke rekening tanpa daftar; hasil cek ditandai terpakai. Hanya untuk kunci berizin payouts:any_destination (selain itu 403).

amountinteger

Nominal yang DITERIMA rekening (batas sama dengan POST /payouts/quote). Biaya transfer dipotong dari saldo di luar nominal ini.

(min 1, maks 1000000000)

withdraw_allboolean

Cairkan seluruh saldo yang bisa dicairkan, dihitung dengan biaya transfer saat pencairan dibuat — saldo tersedia habis tepat bila tidak dibatasi limit harian/maksimum bank.

(default false)

notestring

Dikirim sebagai keterangan transfer; tampil di mutasi penerima bila didukung bank.

(panjang maks 50)

Respons

  • 201

    Dibuat. Status PROCESSING, atau PENDING_APPROVAL bila di atas ambang persetujuan.

    Field respons (21)
    FieldTipeKeterangan
    idwajibstring (uuid)
    referencewajibstring

    Referensi publik pencairan.

    statuswajibstring

    PENDING_APPROVAL menunggu admin ZePay · PROCESSING sedang dikirim ke bank tujuan · SUCCESS diterima rekening · FAILED gagal · CANCELLED dibatalkan merchant · REJECTED ditolak admin.

    PENDING_APPROVALPROCESSINGSUCCESSFAILEDCANCELLEDREJECTED
    amountwajibinteger

    Nominal (porsi merchant ini) yang dikirim ke rekening tujuan.

    (min 0)

    feewajibinteger

    Biaya transfer yang dibebankan ke saldo merchant ini. Sebelum SUCCESS = biaya yang ditahan (hasil pengecekan ke bank tujuan, batas atas); setelah SUCCESS = biaya final (bila lebih kecil, selisihnya sudah dikembalikan). Pada pencairan gabungan = porsi biaya merchant ini; 0 bila ditanggung ZePay.

    (min 0)

    total_debitedwajibinteger

    amount + fee. Kembali utuh ke saldo tersedia bila FAILED/CANCELLED/REJECTED.

    (min 0)

    combinedwajibboolean

    true bila pencairan gabungan dari beberapa merchant yang dibuat admin.

    destinationwajibPayoutDestinationMerchantAccount | PayoutDestinationInquiryAccount | PayoutDestinationZepayAccount
    destination.type"zepay_account"
    destination.bank_account_idnull
    destination.bank_codestring
    destination.bank_namestring
    destination.account_typestring
    bankewallet
    destination.account_namestring
    destination.account_number_maskedstring
    destination.inquiry_idstring (uuid)
    notewajibstring | null
    failure_reasonwajibstring | null

    Alasan bila FAILED / CANCELLED / REJECTED.

    livemodewajibboolean
    created_atwajibstring (date-time)
    completed_atwajibstring (date-time) | null
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    bank_account_not_found — rekening tidak terdaftar untuk merchant ini; atau inquiry_not_found — hasil cek rekening tidak ditemukan (bukan milik merchant/mode ini)

  • 409

    idempotency_conflict — Idempotency-Key sudah dipakai untuk request berbeda, atau request pertama masih diproses

  • 422

    Pencairan tidak bisa dibuat: bank_account_not_verified, insufficient_balance (details: amount, fee, total_debited, available; tanpa amount/total_debited bila withdraw_all), payout_limit_exceeded, amount_below_minimum, amount_above_maximum, merchant_inactive, bank_unavailable, atau account_not_found (rekening tujuan tidak lagi ditemukan saat pengecekan biaya ke bank tujuan, atau hasil cek inquiry_id bukan rekening valid). Dengan inquiry_id: inquiry_expired (lewat 15 menit) atau inquiry_used (sudah dipakai) — cek ulang rekening.

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

  • 503

    provider_unavailable — bank atau kanal pembayaran sedang tidak merespons; coba lagi. Cek rekening: ulangi cek. Pencairan: tidak dibuat (aman diulang dengan Idempotency-Key yang sama). Tagihan: tagihan ditutup FAILED (details.payment_id, details.reference) — buat tagihan baru.

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/payouts' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Idempotency-Key: 0b9f3c1e-7a52-4d0e-9f4e-2c1d8a6b3e70' \
  -H 'Content-Type: application/json' \
  -d '{"bank_account_id":"6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11","amount":100000,"note":"Tarik mingguan"}'
nominal
{
  "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "amount": 100000,
  "note": "Tarik mingguan"
}
semua
{
  "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "withdraw_all": true
}
Tanpa daftar (izin payouts:any_destination)
{
  "inquiry_id": "3b8e1f52-7c0a-4d9e-b6a1-5f2c8d4e9a03",
  "amount": 100000,
  "note": "Bayar vendor"
}

Contoh respons 201

JSON
{
  "id": "9d0f3c1a-2b7e-4f55-8a19-6c3e2d1b0a77",
  "reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
  "status": "PROCESSING",
  "amount": 100000,
  "fee": 2500,
  "total_debited": 102500,
  "combined": false,
  "destination": {
    "type": "merchant_account",
    "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
    "bank_code": "014",
    "bank_name": "BCA",
    "account_type": "bank",
    "account_name": "TOKO BATIK SEKAR",
    "account_number_masked": "•••• 4821"
  },
  "note": "Tarik mingguan",
  "failure_reason": null,
  "livemode": true,
  "created_at": "2026-10-31T10:43:05+07:00",
  "completed_at": null
}
GET/payouts/{id}#

Detail pencairan

Scope payouts:read

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 200

    OK

    Field respons (21)
    FieldTipeKeterangan
    idwajibstring (uuid)
    referencewajibstring

    Referensi publik pencairan.

    statuswajibstring

    PENDING_APPROVAL menunggu admin ZePay · PROCESSING sedang dikirim ke bank tujuan · SUCCESS diterima rekening · FAILED gagal · CANCELLED dibatalkan merchant · REJECTED ditolak admin.

    PENDING_APPROVALPROCESSINGSUCCESSFAILEDCANCELLEDREJECTED
    amountwajibinteger

    Nominal (porsi merchant ini) yang dikirim ke rekening tujuan.

    (min 0)

    feewajibinteger

    Biaya transfer yang dibebankan ke saldo merchant ini. Sebelum SUCCESS = biaya yang ditahan (hasil pengecekan ke bank tujuan, batas atas); setelah SUCCESS = biaya final (bila lebih kecil, selisihnya sudah dikembalikan). Pada pencairan gabungan = porsi biaya merchant ini; 0 bila ditanggung ZePay.

    (min 0)

    total_debitedwajibinteger

    amount + fee. Kembali utuh ke saldo tersedia bila FAILED/CANCELLED/REJECTED.

    (min 0)

    combinedwajibboolean

    true bila pencairan gabungan dari beberapa merchant yang dibuat admin.

    destinationwajibPayoutDestinationMerchantAccount | PayoutDestinationInquiryAccount | PayoutDestinationZepayAccount
    destination.type"zepay_account"
    destination.bank_account_idnull
    destination.bank_codestring
    destination.bank_namestring
    destination.account_typestring
    bankewallet
    destination.account_namestring
    destination.account_number_maskedstring
    destination.inquiry_idstring (uuid)
    notewajibstring | null
    failure_reasonwajibstring | null

    Alasan bila FAILED / CANCELLED / REJECTED.

    livemodewajibboolean
    created_atwajibstring (date-time)
    completed_atwajibstring (date-time) | null
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/payouts/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "id": "9d0f3c1a-2b7e-4f55-8a19-6c3e2d1b0a77",
  "reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
  "status": "PROCESSING",
  "amount": 100000,
  "fee": 2500,
  "total_debited": 102500,
  "combined": false,
  "destination": {
    "type": "merchant_account",
    "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
    "bank_code": "014",
    "bank_name": "BCA",
    "account_type": "bank",
    "account_name": "TOKO BATIK SEKAR",
    "account_number_masked": "•••• 4821"
  },
  "note": "Tarik mingguan",
  "failure_reason": null,
  "livemode": true,
  "created_at": "2026-10-31T10:43:05+07:00",
  "completed_at": null
}
POST/payouts/{id}/cancel#

Batalkan pencairan yang masih menunggu persetujuan

Scope payouts:writeWajib Idempotency-Key

Hanya untuk pencairan PENDING_APPROVAL yang dibuat lewat API. Body tidak diperlukan. Webhook payout.cancelled dikirim.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 200

    Dibatalkan (CANCELLED); nominal dan biaya kembali ke saldo tersedia

    Field respons (21)
    FieldTipeKeterangan
    idwajibstring (uuid)
    referencewajibstring

    Referensi publik pencairan.

    statuswajibstring

    PENDING_APPROVAL menunggu admin ZePay · PROCESSING sedang dikirim ke bank tujuan · SUCCESS diterima rekening · FAILED gagal · CANCELLED dibatalkan merchant · REJECTED ditolak admin.

    PENDING_APPROVALPROCESSINGSUCCESSFAILEDCANCELLEDREJECTED
    amountwajibinteger

    Nominal (porsi merchant ini) yang dikirim ke rekening tujuan.

    (min 0)

    feewajibinteger

    Biaya transfer yang dibebankan ke saldo merchant ini. Sebelum SUCCESS = biaya yang ditahan (hasil pengecekan ke bank tujuan, batas atas); setelah SUCCESS = biaya final (bila lebih kecil, selisihnya sudah dikembalikan). Pada pencairan gabungan = porsi biaya merchant ini; 0 bila ditanggung ZePay.

    (min 0)

    total_debitedwajibinteger

    amount + fee. Kembali utuh ke saldo tersedia bila FAILED/CANCELLED/REJECTED.

    (min 0)

    combinedwajibboolean

    true bila pencairan gabungan dari beberapa merchant yang dibuat admin.

    destinationwajibPayoutDestinationMerchantAccount | PayoutDestinationInquiryAccount | PayoutDestinationZepayAccount
    destination.type"zepay_account"
    destination.bank_account_idnull
    destination.bank_codestring
    destination.bank_namestring
    destination.account_typestring
    bankewallet
    destination.account_namestring
    destination.account_number_maskedstring
    destination.inquiry_idstring (uuid)
    notewajibstring | null
    failure_reasonwajibstring | null

    Alasan bila FAILED / CANCELLED / REJECTED.

    livemodewajibboolean
    created_atwajibstring (date-time)
    completed_atwajibstring (date-time) | null
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 409

    payout_not_cancellable — status bukan PENDING_APPROVAL atau pencairan gabungan dari konsol admin; atau idempotency_conflict

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/payouts/{id}/cancel' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Idempotency-Key: 0b9f3c1e-7a52-4d0e-9f4e-2c1d8a6b3e70'

Contoh respons 200

JSON
{
  "id": "9d0f3c1a-2b7e-4f55-8a19-6c3e2d1b0a77",
  "reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
  "status": "PROCESSING",
  "amount": 100000,
  "fee": 2500,
  "total_debited": 102500,
  "combined": false,
  "destination": {
    "type": "merchant_account",
    "bank_account_id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
    "bank_code": "014",
    "bank_name": "BCA",
    "account_type": "bank",
    "account_name": "TOKO BATIK SEKAR",
    "account_number_masked": "•••• 4821"
  },
  "note": "Tarik mingguan",
  "failure_reason": null,
  "livemode": true,
  "created_at": "2026-10-31T10:43:05+07:00",
  "completed_at": null
}

Transaksi

Tagihan pembayaran QRIS, VA, e-wallet, retail, dan payment link — buat, cari, cek status, batalkan, simulasi.

GET/payments#

Daftar pembayaran

Scope payments:read

Parameter

NamaDiTipeKeterangan
limitqueryinteger

(min 1, maks 100, default 20)

starting_afterquerystring

Kursor dari next_cursor halaman sebelumnya (UUID; btx_… untuk mutasi; evt_… untuk event).

statusquerystring

PENDING menunggu pembayaran · SUCCESS dibayar (dana masuk saldo tertunda, lalu tersedia setelah settlement; VA langsung tersedia) · FAILED ditolak penerbit atau gagal diterbitkan kanal pembayaran · EXPIRED lewat batas waktu (ditutup worker ±2 menit setelah expires_at) · CANCELLED dibatalkan merchant. EXPIRED/CANCELLED bisa menjadi SUCCESS bila dana tetap masuk.

Nilai: PENDING, SUCCESS, FAILED, EXPIRED, CANCELLED

methodquerystring

LINK = semua payment link (apa pun metode yang akhirnya dipilih pelanggan).

Nilai: LINK, QRIS, VA, EWALLET, RETAIL

referencequerystring

Cari satu invoice (persis), mis. SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E.

created_fromquerystring (date-time)

Sejak (inklusif), ISO 8601, mis. 2026-10-01T00:00:00+07:00 (tulis + sebagai %2B).

created_toquerystring (date-time)

Sampai (eksklusif), ISO 8601.

Respons

  • 200

    OK

    Field respons (30)
    FieldTipeKeterangan
    has_morewajibboolean
    next_cursorwajibstring | null

    id item terakhir bila has_more; kirim sebagai starting_after.

    datawajibarray<Payment>
    data[].idwajibstring (uuid)
    data[].referencewajibstring

    Referensi publik tagihan (invoice) = prefix merchant + ULID.

    data[].statuswajibstring

    PENDING menunggu pembayaran · SUCCESS dibayar (dana masuk saldo tertunda, lalu tersedia setelah settlement; VA langsung tersedia) · FAILED ditolak penerbit atau gagal diterbitkan kanal pembayaran · EXPIRED lewat batas waktu (ditutup worker ±2 menit setelah expires_at) · CANCELLED dibatalkan merchant. EXPIRED/CANCELLED bisa menjadi SUCCESS bila dana tetap masuk.

    PENDINGSUCCESSFAILEDEXPIREDCANCELLED
    data[].amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    data[].feewajibinteger | null

    Biaya transaksi kanal pembayaran. Sebelum SUCCESS: nilai yang ditetapkan saat instrumen (QRIS / nomor VA / tautan / kode bayar) dibuat (null bila belum ada, mis. payment link yang belum dipilih metodenya). Setelah SUCCESS: biaya final.

    (min 0)

    data[].fee_bearerwajibstring

    Penanggung biaya transaksi menurut aturan kanal (GET /merchant → fee_paid_by). Payment link mengikuti kanal yang dipilih pelanggan.

    merchantcustomer
    data[].net_amountwajibinteger | null

    Yang masuk saldo merchant — amount − fee bila fee_bearer = merchant, amount bila customer; null bila fee belum diketahui.

    data[].methodwajibstring | null

    null = payment link yang belum dipilih metodenya. CARD/CASH_WITHDRAWAL hanya pada data hasil migrasi v1.

    QRISVAEWALLETRETAILCARDCASH_WITHDRAWAL
    data[].channelwajibstring | null

    QRIS, kode bank VA (014), DANA, ALFAMART, …

    data[].checkout_urlwajibstring (uri) | null

    Halaman checkout ZePay (hanya LINK).

    data[].allowed_methodswajibarray<string> | null
    data[].qr_stringwajibstring | null

    Isi QRIS dinamis (render sebagai QR).

    data[].va_numberwajibstring | null
    data[].payment_urlwajibstring (uri) | null

    Tautan pembayaran e-wallet (bukan untuk OVO push).

    data[].payment_codewajibstring | null

    Kode bayar retail (Alfamart/Indomaret).

    data[].descriptionwajibstring | null
    data[].customerwajibobject
    data[].customer.namestring
    data[].customer.emailstring
    data[].customer.phonestring
    data[].expires_atwajibstring (date-time) | null
    data[].paid_atwajibstring (date-time) | null
    data[].settled_atwajibstring (date-time) | null

    Terisi setelah settlement (dana pindah ke saldo tersedia).

    data[].failure_reasonwajibstring | null

    Alasan bila EXPIRED / FAILED / CANCELLED

    data[].metadatawajibobject
    data[].livemodewajibboolean
    data[].created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/payments' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "has_more": true,
  "next_cursor": "string",
  "data": [
    {
      "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
      "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
      "status": "PENDING",
      "amount": 0,
      "fee": 0,
      "fee_bearer": "merchant",
      "net_amount": 0,
      "method": "QRIS",
      "channel": "string",
      "checkout_url": "https://toko.example/kembali",
      "allowed_methods": [
        "QRIS"
      ],
      "qr_string": "string",
      "va_number": "string",
      "payment_url": "https://toko.example/kembali",
      "payment_code": "string",
      "description": "string",
      "customer": {
        "name": "string",
        "email": "string",
        "phone": "string"
      },
      "expires_at": "2026-10-31T10:42:05+07:00",
      "paid_at": "2026-10-31T10:42:05+07:00",
      "settled_at": "2026-10-31T10:42:05+07:00",
      "failure_reason": "Tagihan kedaluwarsa",
      "metadata": {},
      "livemode": true,
      "created_at": "2026-10-31T10:42:05+07:00"
    }
  ]
}
POST/payments#

Buat tagihan

Scope payments:writeWajib Idempotency-Key

method = LINK membuat halaman checkout ZePay (pelanggan memilih metode dari allowed_methods yang aktif dan menerima nominal itu). Metode lain langsung menerbitkan QRIS / nomor VA / tautan e-wallet / kode retail di kanal pembayaran; fee di respons = biaya transaksi kanal itu. Bila kanal pembayaran menolak atau tidak merespons, tagihan langsung ditutup FAILED dan details.payment_id / details.reference menunjuk tagihan itu (422 payment_method_unavailable dengan details.reason_code, atau 503 provider_unavailable).

Body JSON

FieldTipeKeterangan
amountwajibinteger

Nominal tagihan. Batas kanal (min_amount/max_amount di GET /merchant) juga berlaku (422).

(min 1000, maks 100000000)

methodwajibstring
LINKQRISVAEWALLETRETAIL
channelstring

Wajib untuk VA, EWALLET, RETAIL (QRIS: abaikan). VA: kode bank (014) atau nama bank (BCA) — respons selalu memakai kode bank. EWALLET: DANA, SHOPEEPAY, LINKAJA, OVO (OVO wajib customer.phone). RETAIL: ALFAMART, INDOMARET. Kanal aktif: GET /merchant.

(panjang min 2, panjang maks 20)

allowed_methodsarray<string>

Hanya untuk LINK; bawaan semua metode. Yang tidak punya kanal aktif untuk nominal ini dibuang.

(min 1 item, maks 4 item)

expires_in_minutesinteger

Bawaan expiry_minutes kanal (LINK: 1440).

(min 5, maks 10080)

descriptionstring

(panjang maks 140)

customerobject
customer.namestring

(panjang maks 60)

customer.emailstring (email)

(panjang maks 100)

customer.phonestring

Pola: ^08[0-9]{8,11}$

redirect_url_successstring (uri)

Bawaan dari pengaturan merchant.

Pola: ^https://\S+$

(panjang maks 500)

redirect_url_failedstring (uri)

Bawaan dari pengaturan merchant.

Pola: ^https://\S+$

(panjang maks 500)

metadataobject

Dikembalikan di objek Payment & event pembayaran, mis. order_id dari sistem Anda. Kunci maks. 40 karakter, total JSON maks. 4.000 karakter.

Respons

  • 201

    Dibuat

    Field respons (27)
    FieldTipeKeterangan
    idwajibstring (uuid)
    referencewajibstring

    Referensi publik tagihan (invoice) = prefix merchant + ULID.

    statuswajibstring

    PENDING menunggu pembayaran · SUCCESS dibayar (dana masuk saldo tertunda, lalu tersedia setelah settlement; VA langsung tersedia) · FAILED ditolak penerbit atau gagal diterbitkan kanal pembayaran · EXPIRED lewat batas waktu (ditutup worker ±2 menit setelah expires_at) · CANCELLED dibatalkan merchant. EXPIRED/CANCELLED bisa menjadi SUCCESS bila dana tetap masuk.

    PENDINGSUCCESSFAILEDEXPIREDCANCELLED
    amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    feewajibinteger | null

    Biaya transaksi kanal pembayaran. Sebelum SUCCESS: nilai yang ditetapkan saat instrumen (QRIS / nomor VA / tautan / kode bayar) dibuat (null bila belum ada, mis. payment link yang belum dipilih metodenya). Setelah SUCCESS: biaya final.

    (min 0)

    fee_bearerwajibstring

    Penanggung biaya transaksi menurut aturan kanal (GET /merchant → fee_paid_by). Payment link mengikuti kanal yang dipilih pelanggan.

    merchantcustomer
    net_amountwajibinteger | null

    Yang masuk saldo merchant — amount − fee bila fee_bearer = merchant, amount bila customer; null bila fee belum diketahui.

    methodwajibstring | null

    null = payment link yang belum dipilih metodenya. CARD/CASH_WITHDRAWAL hanya pada data hasil migrasi v1.

    QRISVAEWALLETRETAILCARDCASH_WITHDRAWAL
    channelwajibstring | null

    QRIS, kode bank VA (014), DANA, ALFAMART, …

    checkout_urlwajibstring (uri) | null

    Halaman checkout ZePay (hanya LINK).

    allowed_methodswajibarray<string> | null
    qr_stringwajibstring | null

    Isi QRIS dinamis (render sebagai QR).

    va_numberwajibstring | null
    payment_urlwajibstring (uri) | null

    Tautan pembayaran e-wallet (bukan untuk OVO push).

    payment_codewajibstring | null

    Kode bayar retail (Alfamart/Indomaret).

    descriptionwajibstring | null
    customerwajibobject
    customer.namestring
    customer.emailstring
    customer.phonestring
    expires_atwajibstring (date-time) | null
    paid_atwajibstring (date-time) | null
    settled_atwajibstring (date-time) | null

    Terisi setelah settlement (dana pindah ke saldo tersedia).

    failure_reasonwajibstring | null

    Alasan bila EXPIRED / FAILED / CANCELLED

    metadatawajibobject
    livemodewajibboolean
    created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 409

    idempotency_conflict — Idempotency-Key sudah dipakai untuk request berbeda, atau request pertama masih diproses

  • 422

    Tagihan tidak bisa dibuat: merchant_inactive; payment_method_unavailable (kanal tidak ada/nonaktif, tidak ada metode aktif yang menerima nominal itu untuk LINK — details.allowed_methods, atau kanal pembayaran menolak membuat tagihan — tagihan ditutup FAILED, details.payment_id, reference, reason_code); amount_below_minimum (details.min_amount) atau amount_above_maximum (details.max_amount) menurut batas kanal.

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

  • 503

    provider_unavailable — bank atau kanal pembayaran sedang tidak merespons; coba lagi. Cek rekening: ulangi cek. Pencairan: tidak dibuat (aman diulang dengan Idempotency-Key yang sama). Tagihan: tagihan ditutup FAILED (details.payment_id, details.reference) — buat tagihan baru.

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/payments' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Idempotency-Key: 0b9f3c1e-7a52-4d0e-9f4e-2c1d8a6b3e70' \
  -H 'Content-Type: application/json' \
  -d '{"amount":250000,"method":"QRIS","description":"Pesanan #1042","metadata":{"order_id":"1042"}}'
qris
{
  "amount": 250000,
  "method": "QRIS",
  "description": "Pesanan #1042",
  "metadata": {
    "order_id": "1042"
  }
}
va
{
  "amount": 150000,
  "method": "VA",
  "channel": "BCA",
  "customer": {
    "name": "Budi Santoso",
    "phone": "081234567788"
  },
  "metadata": {
    "order_id": "1043"
  }
}
paymentLink
{
  "amount": 50000,
  "method": "LINK",
  "allowed_methods": [
    "QRIS",
    "VA"
  ],
  "expires_in_minutes": 1440,
  "redirect_url_success": "https://tokobatik.id/terima-kasih"
}

Contoh respons 201

qris
{
  "id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
  "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
  "status": "PENDING",
  "amount": 250000,
  "fee": 1750,
  "fee_bearer": "merchant",
  "net_amount": 248250,
  "method": "QRIS",
  "channel": "QRIS",
  "checkout_url": null,
  "allowed_methods": null,
  "qr_string": "00020101021226650013ID.CO.QRIS.WWW…6304A1B2",
  "va_number": null,
  "payment_url": null,
  "payment_code": null,
  "description": "Pesanan #1042",
  "customer": {},
  "expires_at": "2026-11-01T10:30:00+07:00",
  "paid_at": null,
  "settled_at": null,
  "failure_reason": null,
  "metadata": {
    "order_id": "1042"
  },
  "livemode": true,
  "created_at": "2026-10-31T10:30:00+07:00"
}
paymentLink
{
  "id": "7e3b9d21-4c5a-4f80-9b12-3d6e8a0c1f45",
  "reference": "SKR-01JBQ7Z9H4N2C5V8D1F6K3M0QT",
  "status": "PENDING",
  "amount": 50000,
  "fee": null,
  "fee_bearer": "merchant",
  "net_amount": null,
  "method": null,
  "channel": null,
  "checkout_url": "https://payment.envfri.com/pay/EFoxFkkUEsaVYrGkSEa2eBel",
  "allowed_methods": [
    "QRIS",
    "VA"
  ],
  "qr_string": null,
  "va_number": null,
  "payment_url": null,
  "payment_code": null,
  "description": null,
  "customer": {},
  "expires_at": "2026-11-01T10:31:00+07:00",
  "paid_at": null,
  "settled_at": null,
  "failure_reason": null,
  "metadata": {},
  "livemode": true,
  "created_at": "2026-10-31T10:31:00+07:00"
}
GET/payments/{id}#

Detail pembayaran

Scope payments:read

Cadangan bila webhook terlambat. fee final setelah SUCCESS; settled_at terisi setelah settlement.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 200

    OK

    Field respons (27)
    FieldTipeKeterangan
    idwajibstring (uuid)
    referencewajibstring

    Referensi publik tagihan (invoice) = prefix merchant + ULID.

    statuswajibstring

    PENDING menunggu pembayaran · SUCCESS dibayar (dana masuk saldo tertunda, lalu tersedia setelah settlement; VA langsung tersedia) · FAILED ditolak penerbit atau gagal diterbitkan kanal pembayaran · EXPIRED lewat batas waktu (ditutup worker ±2 menit setelah expires_at) · CANCELLED dibatalkan merchant. EXPIRED/CANCELLED bisa menjadi SUCCESS bila dana tetap masuk.

    PENDINGSUCCESSFAILEDEXPIREDCANCELLED
    amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    feewajibinteger | null

    Biaya transaksi kanal pembayaran. Sebelum SUCCESS: nilai yang ditetapkan saat instrumen (QRIS / nomor VA / tautan / kode bayar) dibuat (null bila belum ada, mis. payment link yang belum dipilih metodenya). Setelah SUCCESS: biaya final.

    (min 0)

    fee_bearerwajibstring

    Penanggung biaya transaksi menurut aturan kanal (GET /merchant → fee_paid_by). Payment link mengikuti kanal yang dipilih pelanggan.

    merchantcustomer
    net_amountwajibinteger | null

    Yang masuk saldo merchant — amount − fee bila fee_bearer = merchant, amount bila customer; null bila fee belum diketahui.

    methodwajibstring | null

    null = payment link yang belum dipilih metodenya. CARD/CASH_WITHDRAWAL hanya pada data hasil migrasi v1.

    QRISVAEWALLETRETAILCARDCASH_WITHDRAWAL
    channelwajibstring | null

    QRIS, kode bank VA (014), DANA, ALFAMART, …

    checkout_urlwajibstring (uri) | null

    Halaman checkout ZePay (hanya LINK).

    allowed_methodswajibarray<string> | null
    qr_stringwajibstring | null

    Isi QRIS dinamis (render sebagai QR).

    va_numberwajibstring | null
    payment_urlwajibstring (uri) | null

    Tautan pembayaran e-wallet (bukan untuk OVO push).

    payment_codewajibstring | null

    Kode bayar retail (Alfamart/Indomaret).

    descriptionwajibstring | null
    customerwajibobject
    customer.namestring
    customer.emailstring
    customer.phonestring
    expires_atwajibstring (date-time) | null
    paid_atwajibstring (date-time) | null
    settled_atwajibstring (date-time) | null

    Terisi setelah settlement (dana pindah ke saldo tersedia).

    failure_reasonwajibstring | null

    Alasan bila EXPIRED / FAILED / CANCELLED

    metadatawajibobject
    livemodewajibboolean
    created_atwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/payments/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
  "status": "PENDING",
  "amount": 0,
  "fee": 0,
  "fee_bearer": "merchant",
  "net_amount": 0,
  "method": "QRIS",
  "channel": "string",
  "checkout_url": "https://toko.example/kembali",
  "allowed_methods": [
    "QRIS"
  ],
  "qr_string": "string",
  "va_number": "string",
  "payment_url": "https://toko.example/kembali",
  "payment_code": "string",
  "description": "string",
  "customer": {
    "name": "string",
    "email": "string",
    "phone": "string"
  },
  "expires_at": "2026-10-31T10:42:05+07:00",
  "paid_at": "2026-10-31T10:42:05+07:00",
  "settled_at": "2026-10-31T10:42:05+07:00",
  "failure_reason": "Tagihan kedaluwarsa",
  "metadata": {},
  "livemode": true,
  "created_at": "2026-10-31T10:42:05+07:00"
}
POST/payments/{id}/cancel#

Batalkan tagihan yang belum dibayar

Scope payments:writeWajib Idempotency-Key

Hanya untuk status PENDING (tagihan yang sudah CANCELLED dikembalikan apa adanya). Webhook payment.cancelled dikirim. Bila dana ternyata tetap masuk (mis. VA dibayar sebelum nomor VA ditutup bank), pembayaran tetap dicatat SUCCESS dan payment.succeeded dikirim — dana tidak hilang.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Body JSON (opsional)

FieldTipeKeterangan
reasonstring

Disimpan di failure_reason; bawaan Dibatalkan merchant.

(panjang maks 140)

Respons

  • 200

    Dibatalkan

    Field respons (27)
    FieldTipeKeterangan
    idwajibstring (uuid)
    referencewajibstring

    Referensi publik tagihan (invoice) = prefix merchant + ULID.

    statuswajibstring

    PENDING menunggu pembayaran · SUCCESS dibayar (dana masuk saldo tertunda, lalu tersedia setelah settlement; VA langsung tersedia) · FAILED ditolak penerbit atau gagal diterbitkan kanal pembayaran · EXPIRED lewat batas waktu (ditutup worker ±2 menit setelah expires_at) · CANCELLED dibatalkan merchant. EXPIRED/CANCELLED bisa menjadi SUCCESS bila dana tetap masuk.

    PENDINGSUCCESSFAILEDEXPIREDCANCELLED
    amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    feewajibinteger | null

    Biaya transaksi kanal pembayaran. Sebelum SUCCESS: nilai yang ditetapkan saat instrumen (QRIS / nomor VA / tautan / kode bayar) dibuat (null bila belum ada, mis. payment link yang belum dipilih metodenya). Setelah SUCCESS: biaya final.

    (min 0)

    fee_bearerwajibstring

    Penanggung biaya transaksi menurut aturan kanal (GET /merchant → fee_paid_by). Payment link mengikuti kanal yang dipilih pelanggan.

    merchantcustomer
    net_amountwajibinteger | null

    Yang masuk saldo merchant — amount − fee bila fee_bearer = merchant, amount bila customer; null bila fee belum diketahui.

    methodwajibstring | null

    null = payment link yang belum dipilih metodenya. CARD/CASH_WITHDRAWAL hanya pada data hasil migrasi v1.

    QRISVAEWALLETRETAILCARDCASH_WITHDRAWAL
    channelwajibstring | null

    QRIS, kode bank VA (014), DANA, ALFAMART, …

    checkout_urlwajibstring (uri) | null

    Halaman checkout ZePay (hanya LINK).

    allowed_methodswajibarray<string> | null
    qr_stringwajibstring | null

    Isi QRIS dinamis (render sebagai QR).

    va_numberwajibstring | null
    payment_urlwajibstring (uri) | null

    Tautan pembayaran e-wallet (bukan untuk OVO push).

    payment_codewajibstring | null

    Kode bayar retail (Alfamart/Indomaret).

    descriptionwajibstring | null
    customerwajibobject
    customer.namestring
    customer.emailstring
    customer.phonestring
    expires_atwajibstring (date-time) | null
    paid_atwajibstring (date-time) | null
    settled_atwajibstring (date-time) | null

    Terisi setelah settlement (dana pindah ke saldo tersedia).

    failure_reasonwajibstring | null

    Alasan bila EXPIRED / FAILED / CANCELLED

    metadatawajibobject
    livemodewajibboolean
    created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 409

    payment_not_cancellable — status sudah bukan PENDING; atau idempotency_conflict

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/payments/{id}/cancel' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Idempotency-Key: 0b9f3c1e-7a52-4d0e-9f4e-2c1d8a6b3e70' \
  -H 'Content-Type: application/json' \
  -d '{"reason":"Pesanan dibatalkan pembeli"}'

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
  "status": "PENDING",
  "amount": 0,
  "fee": 0,
  "fee_bearer": "merchant",
  "net_amount": 0,
  "method": "QRIS",
  "channel": "string",
  "checkout_url": "https://toko.example/kembali",
  "allowed_methods": [
    "QRIS"
  ],
  "qr_string": "string",
  "va_number": "string",
  "payment_url": "https://toko.example/kembali",
  "payment_code": "string",
  "description": "string",
  "customer": {
    "name": "string",
    "email": "string",
    "phone": "string"
  },
  "expires_at": "2026-10-31T10:42:05+07:00",
  "paid_at": "2026-10-31T10:42:05+07:00",
  "settled_at": "2026-10-31T10:42:05+07:00",
  "failure_reason": "Tagihan kedaluwarsa",
  "metadata": {},
  "livemode": true,
  "created_at": "2026-10-31T10:42:05+07:00"
}
POST/payments/{id}/simulate#

Simulasikan hasil pembayaran (mode uji)

Scope payments:write

Hanya untuk kunci zpk_test_… — kunci live ditolak 403 permission_denied. Meniru hasil pembayaran dari kanal pembayaran untuk instrumen terakhir tagihan ini, lalu mengembalikan objek Payment terbaru:

  • SUCCESS → status SUCCESS, fee final = biaya simulasi, dana masuk saldo tertunda, webhook payment.succeeded. Settlement disimulasikan ±1 menit kemudian (settled_at, balance.settled, tertunda → tersedia). Tagihan yang sudah SUCCESS tidak berubah; tagihan EXPIRED/CANCELLED menjadi SUCCESS (meniru dana yang tetap masuk).
  • FAILED / EXPIRED → tagihan langsung ditutup dengan status itu (payment.failed / payment.expired). Pada payment link, hanya percobaan metode itu yang gagal; tagihan tetap PENDING sehingga pelanggan bisa memilih metode lain.

Payment link yang belum dipilih metodenya di halaman checkout belum punya instrumen → 400 invalid_request. Tidak memakai Idempotency-Key.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Body JSON (opsional)

FieldTipeKeterangan
statusstring

(default "SUCCESS")

SUCCESSFAILEDEXPIRED

Respons

  • 200

    Hasil simulasi diterapkan

    Field respons (27)
    FieldTipeKeterangan
    idwajibstring (uuid)
    referencewajibstring

    Referensi publik tagihan (invoice) = prefix merchant + ULID.

    statuswajibstring

    PENDING menunggu pembayaran · SUCCESS dibayar (dana masuk saldo tertunda, lalu tersedia setelah settlement; VA langsung tersedia) · FAILED ditolak penerbit atau gagal diterbitkan kanal pembayaran · EXPIRED lewat batas waktu (ditutup worker ±2 menit setelah expires_at) · CANCELLED dibatalkan merchant. EXPIRED/CANCELLED bisa menjadi SUCCESS bila dana tetap masuk.

    PENDINGSUCCESSFAILEDEXPIREDCANCELLED
    amountwajibinteger

    Rupiah, bilangan bulat.

    (min 0)

    feewajibinteger | null

    Biaya transaksi kanal pembayaran. Sebelum SUCCESS: nilai yang ditetapkan saat instrumen (QRIS / nomor VA / tautan / kode bayar) dibuat (null bila belum ada, mis. payment link yang belum dipilih metodenya). Setelah SUCCESS: biaya final.

    (min 0)

    fee_bearerwajibstring

    Penanggung biaya transaksi menurut aturan kanal (GET /merchant → fee_paid_by). Payment link mengikuti kanal yang dipilih pelanggan.

    merchantcustomer
    net_amountwajibinteger | null

    Yang masuk saldo merchant — amount − fee bila fee_bearer = merchant, amount bila customer; null bila fee belum diketahui.

    methodwajibstring | null

    null = payment link yang belum dipilih metodenya. CARD/CASH_WITHDRAWAL hanya pada data hasil migrasi v1.

    QRISVAEWALLETRETAILCARDCASH_WITHDRAWAL
    channelwajibstring | null

    QRIS, kode bank VA (014), DANA, ALFAMART, …

    checkout_urlwajibstring (uri) | null

    Halaman checkout ZePay (hanya LINK).

    allowed_methodswajibarray<string> | null
    qr_stringwajibstring | null

    Isi QRIS dinamis (render sebagai QR).

    va_numberwajibstring | null
    payment_urlwajibstring (uri) | null

    Tautan pembayaran e-wallet (bukan untuk OVO push).

    payment_codewajibstring | null

    Kode bayar retail (Alfamart/Indomaret).

    descriptionwajibstring | null
    customerwajibobject
    customer.namestring
    customer.emailstring
    customer.phonestring
    expires_atwajibstring (date-time) | null
    paid_atwajibstring (date-time) | null
    settled_atwajibstring (date-time) | null

    Terisi setelah settlement (dana pindah ke saldo tersedia).

    failure_reasonwajibstring | null

    Alasan bila EXPIRED / FAILED / CANCELLED

    metadatawajibobject
    livemodewajibboolean
    created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — dipanggil dengan kunci live, kunci tanpa scope payments:write, atau IP tidak diizinkan

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/payments/{id}/simulate' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Content-Type: application/json' \
  -d '{"status":"SUCCESS"}'
sukses
{
  "status": "SUCCESS"
}
gagal
{
  "status": "FAILED"
}

Contoh respons 200

sukses
{
  "id": "4a8c2e6f-1b3d-4f57-9a0c-2e4f6a8c0b13",
  "reference": "SKR-01JBQA3C6E9G2J5M8P1R4T7W0Y",
  "status": "SUCCESS",
  "amount": 250000,
  "fee": 1750,
  "fee_bearer": "merchant",
  "net_amount": 248250,
  "method": "QRIS",
  "channel": "QRIS",
  "checkout_url": null,
  "allowed_methods": null,
  "qr_string": "00020101021226650013ID.ZEPAY.SIM…6304SIMU",
  "va_number": null,
  "payment_url": null,
  "payment_code": null,
  "description": null,
  "customer": {},
  "expires_at": "2026-11-01T10:30:00+07:00",
  "paid_at": "2026-10-31T10:31:02+07:00",
  "settled_at": null,
  "failure_reason": null,
  "metadata": {},
  "livemode": false,
  "created_at": "2026-10-31T10:30:00+07:00"
}

Webhook & event

Endpoint webhook milik merchant, rotasi secret, event uji, riwayat event & kirim ulang.

GET/webhook-endpoints#

Endpoint webhook milik merchant

Scope webhooks:read

Semua endpoint di mode kunci ini (maks. 3), urut waktu dibuat. Tanpa paginasi.

Respons

  • 200

    OK

    Field respons (11)
    FieldTipeKeterangan
    datawajibarray<WebhookEndpoint>
    data[].idwajibstring (uuid)
    data[].urlwajibstring (uri)
    data[].eventswajibarray<string>
    data[].descriptionwajibstring | null
    data[].statuswajibstring
    enableddisabled
    data[].formatwajibstring

    v2 = event bertanda tangan (lihat skema Event). v1_legacy hanya untuk endpoint hasil migrasi v1 (disetel ZePay, tidak bisa lewat API) selama masa transisi: body lama {partner_reff, status, amount} dan hanya untuk event payment.* (event lain dilewati).

    v2v1_legacy
    data[].secret_last4wajibstring

    4 karakter terakhir secret aktif

    data[].previous_secret_expires_atwajibstring (date-time) | null

    Akhir masa transisi rotasi

    data[].livemodewajibboolean
    data[].created_atwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/webhook-endpoints' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "data": [
    {
      "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
      "url": "https://toko.example/kembali",
      "events": [
        "string"
      ],
      "description": "string",
      "status": "enabled",
      "format": "v2",
      "secret_last4": "string",
      "previous_secret_expires_at": "2026-10-31T10:42:05+07:00",
      "livemode": true,
      "created_at": "2026-10-31T10:42:05+07:00"
    }
  ]
}
POST/webhook-endpoints#

Tambah endpoint webhook

Scope webhooks:writeWajib Idempotency-Key

Maks. 3 endpoint per mode (live/uji). secret hanya ditampilkan sekali di respons ini (dan pada pengulangan dengan Idempotency-Key yang sama selama 24 jam).

Body JSON

FieldTipeKeterangan
urlwajibstring (uri)

Pola: ^https://\S+$

(panjang maks 500)

eventsarray<string>

Nama event persis, pola payment.* / payout.* / balance.* / bank_account.* / webhook.*, atau *.

(min 1 item, maks 20 item, default ["payment.","payout.","balance.","bank_account."])

descriptionstring

(panjang maks 100)

Respons

  • 201

    Dibuat

    Field respons (11)
    FieldTipeKeterangan
    idwajibstring (uuid)
    urlwajibstring (uri)
    eventswajibarray<string>
    descriptionwajibstring | null
    statuswajibstring
    enableddisabled
    formatwajibstring

    v2 = event bertanda tangan (lihat skema Event). v1_legacy hanya untuk endpoint hasil migrasi v1 (disetel ZePay, tidak bisa lewat API) selama masa transisi: body lama {partner_reff, status, amount} dan hanya untuk event payment.* (event lain dilewati).

    v2v1_legacy
    secret_last4wajibstring

    4 karakter terakhir secret aktif

    previous_secret_expires_atwajibstring (date-time) | null

    Akhir masa transisi rotasi

    livemodewajibboolean
    created_atwajibstring (date-time)
    secretwajibstring

    Signing secret (whsec_…). Hanya ditampilkan sekali; simpan di server.

  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 409

    webhook_endpoint_limit — sudah ada 3 endpoint di mode ini; atau idempotency_conflict

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/webhook-endpoints' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Idempotency-Key: 0b9f3c1e-7a52-4d0e-9f4e-2c1d8a6b3e70' \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://tokobatik.id/hooks/zepay","description":"Server produksi"}'

Contoh respons 201

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "url": "https://toko.example/kembali",
  "events": [
    "string"
  ],
  "description": "string",
  "status": "enabled",
  "format": "v2",
  "secret_last4": "string",
  "previous_secret_expires_at": "2026-10-31T10:42:05+07:00",
  "livemode": true,
  "created_at": "2026-10-31T10:42:05+07:00",
  "secret": "string"
}
GET/webhook-endpoints/{id}#

Detail endpoint webhook

Scope webhooks:read

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 200

    OK

    Field respons (10)
    FieldTipeKeterangan
    idwajibstring (uuid)
    urlwajibstring (uri)
    eventswajibarray<string>
    descriptionwajibstring | null
    statuswajibstring
    enableddisabled
    formatwajibstring

    v2 = event bertanda tangan (lihat skema Event). v1_legacy hanya untuk endpoint hasil migrasi v1 (disetel ZePay, tidak bisa lewat API) selama masa transisi: body lama {partner_reff, status, amount} dan hanya untuk event payment.* (event lain dilewati).

    v2v1_legacy
    secret_last4wajibstring

    4 karakter terakhir secret aktif

    previous_secret_expires_atwajibstring (date-time) | null

    Akhir masa transisi rotasi

    livemodewajibboolean
    created_atwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/webhook-endpoints/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "url": "https://toko.example/kembali",
  "events": [
    "string"
  ],
  "description": "string",
  "status": "enabled",
  "format": "v2",
  "secret_last4": "string",
  "previous_secret_expires_at": "2026-10-31T10:42:05+07:00",
  "livemode": true,
  "created_at": "2026-10-31T10:42:05+07:00"
}
PATCH/webhook-endpoints/{id}#

Ubah URL, langganan event, deskripsi, atau aktif/nonaktif

Scope webhooks:write

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Body JSON

FieldTipeKeterangan
urlstring (uri)

Pola: ^https://\S+$

(panjang maks 500)

eventsarray<string>

(min 1 item, maks 20 item)

descriptionstring | null

(panjang maks 100)

statusstring
enableddisabled

Respons

  • 200

    Diperbarui

    Field respons (10)
    FieldTipeKeterangan
    idwajibstring (uuid)
    urlwajibstring (uri)
    eventswajibarray<string>
    descriptionwajibstring | null
    statuswajibstring
    enableddisabled
    formatwajibstring

    v2 = event bertanda tangan (lihat skema Event). v1_legacy hanya untuk endpoint hasil migrasi v1 (disetel ZePay, tidak bisa lewat API) selama masa transisi: body lama {partner_reff, status, amount} dan hanya untuk event payment.* (event lain dilewati).

    v2v1_legacy
    secret_last4wajibstring

    4 karakter terakhir secret aktif

    previous_secret_expires_atwajibstring (date-time) | null

    Akhir masa transisi rotasi

    livemodewajibboolean
    created_atwajibstring (date-time)
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X PATCH 'https://payment.envfri.com/api/v2/webhook-endpoints/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Content-Type: application/json' \
  -d '{"status":"disabled"}'
nonaktifkan
{
  "status": "disabled"
}
langganan
{
  "events": [
    "payment.*",
    "payout.*"
  ]
}

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "url": "https://toko.example/kembali",
  "events": [
    "string"
  ],
  "description": "string",
  "status": "enabled",
  "format": "v2",
  "secret_last4": "string",
  "previous_secret_expires_at": "2026-10-31T10:42:05+07:00",
  "livemode": true,
  "created_at": "2026-10-31T10:42:05+07:00"
}
DELETE/webhook-endpoints/{id}#

Hapus endpoint webhook

Scope webhooks:write

Pengiriman yang belum terkirim ke endpoint ini ikut dihapus.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 204

    Dihapus (tanpa body)

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X DELETE 'https://payment.envfri.com/api/v2/webhook-endpoints/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'
POST/webhook-endpoints/{id}/rotate-secret#

Rotasi signing secret webhook

Scope webhooks:writeWajib Idempotency-Key

Secret baru langsung dipakai. Selama masa transisi (previous_secret_valid_hours), setiap event ditandatangani dengan secret baru dan lama (ZePay-Signature: t=…,v1=<baru>,v1=<lama>), jadi server merchant bisa diperbarui tanpa kehilangan event. 0 = secret lama langsung tidak dipakai.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Body JSON (opsional)

FieldTipeKeterangan
previous_secret_valid_hoursinteger

(default 24)

024168

Respons

  • 200

    Secret baru (ditampilkan sekali)

    Field respons (11)
    FieldTipeKeterangan
    idwajibstring (uuid)
    urlwajibstring (uri)
    eventswajibarray<string>
    descriptionwajibstring | null
    statuswajibstring
    enableddisabled
    formatwajibstring

    v2 = event bertanda tangan (lihat skema Event). v1_legacy hanya untuk endpoint hasil migrasi v1 (disetel ZePay, tidak bisa lewat API) selama masa transisi: body lama {partner_reff, status, amount} dan hanya untuk event payment.* (event lain dilewati).

    v2v1_legacy
    secret_last4wajibstring

    4 karakter terakhir secret aktif

    previous_secret_expires_atwajibstring (date-time) | null

    Akhir masa transisi rotasi

    livemodewajibboolean
    created_atwajibstring (date-time)
    secretwajibstring

    Signing secret (whsec_…). Hanya ditampilkan sekali; simpan di server.

  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 409

    idempotency_conflict — Idempotency-Key sudah dipakai untuk request berbeda, atau request pertama masih diproses

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/webhook-endpoints/{id}/rotate-secret' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>' \
  -H 'Idempotency-Key: 0b9f3c1e-7a52-4d0e-9f4e-2c1d8a6b3e70' \
  -H 'Content-Type: application/json' \
  -d '{"previous_secret_valid_hours":24}'

Contoh respons 200

JSON
{
  "id": "6f1c2a0e-4b7d-4c1a-9a51-2f0b3e7c9d11",
  "url": "https://toko.example/kembali",
  "events": [
    "string"
  ],
  "description": "string",
  "status": "enabled",
  "format": "v2",
  "secret_last4": "string",
  "previous_secret_expires_at": "2026-10-31T10:42:05+07:00",
  "livemode": true,
  "created_at": "2026-10-31T10:42:05+07:00",
  "secret": "string"
}
POST/webhook-endpoints/{id}/test#

Kirim event uji `webhook.test` ke endpoint ini

Scope webhooks:write

Event webhook.test dikirim ke endpoint ini saja, juga bila endpoint tidak berlangganan webhook.test. Endpoint nonaktif → pengiriman langsung dead (last_error: endpoint nonaktif). Tidak memakai Idempotency-Key.

Parameter

NamaDiTipeKeterangan
idwajibpathstring (uuid)

UUID objek. Bukan UUID atau bukan milik merchant/mode ini → 404 not_found.

Respons

  • 202

    Dijadwalkan; hasil pengiriman terlihat di GET /events/{event_id}

    Field respons (1)
    FieldTipeKeterangan
    event_idwajibstring
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/webhook-endpoints/{id}/test' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 202

JSON
{
  "event_id": "evt_9b1d3f5a7c9e4b2d8f0a1c3e5a7b9d2f"
}
GET/events#

Riwayat event (untuk mengejar webhook yang terlewat)

Scope webhooks:read

Event 30 hari terakhir, terbaru dulu. Saring webhook yang gagal dengan delivery_status=failed, lalu kirim ulang dengan POST /events/{id}/resend. Kursor starting_after memakai id event (evt_…).

Parameter

NamaDiTipeKeterangan
limitqueryinteger

(min 1, maks 100, default 20)

starting_afterquerystring

Kursor dari next_cursor halaman sebelumnya (UUID; btx_… untuk mutasi; evt_… untuk event).

typequerystring

Nama persis, pola payment.*, atau *.

delivery_statusquerystring

succeeded = minimal satu endpoint menerima · pending = masih dijadwalkan · failed = semua percobaan berhenti (dead) · no_endpoint = tidak ada endpoint yang berlangganan saat event dibuat.

Nilai: pending, succeeded, failed, no_endpoint

created_fromquerystring (date-time)

Sejak (inklusif), ISO 8601, mis. 2026-10-01T00:00:00+07:00 (tulis + sebagai %2B).

created_toquerystring (date-time)

Sampai (eksklusif), ISO 8601.

Respons

  • 200

    OK

    Field respons (9)
    FieldTipeKeterangan
    has_morewajibboolean
    next_cursorwajibstring | null

    id item terakhir bila has_more; kirim sebagai starting_after.

    datawajibarray<EventSummary>
    data[].idwajibstring

    evt_ + 32 heksadesimal

    data[].typewajibstring
    payment.succeededpayment.expiredpayment.failedpayment.cancelledbalance.settledpayout.createdpayout.succeededpayout.failedpayout.cancelledbank_account.createdbank_account.verifiedbank_account.rejectedwebhook.test
    data[].createdwajibstring (date-time)
    data[].livemodewajibboolean
    data[].datawajibobject
    data[].delivery_statuswajibstring

    succeeded bila minimal satu endpoint menerima; failed bila semua pengiriman berhenti (dead); no_endpoint bila tidak ada endpoint yang berlangganan.

    pendingsucceededfailedno_endpoint
  • 400

    invalid_request — body/parameter tidak valid: field wajib kosong, tipe/format salah, field tidak dikenal, nilai di luar batas, JSON rusak, body > 64 KB, atau header Idempotency-Key tidak ada/salah format. Rincian validasi di details.errors.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/events' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "has_more": true,
  "next_cursor": "string",
  "data": [
    {
      "id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
      "type": "payment.succeeded",
      "created": "2026-10-31T10:44:12+07:00",
      "livemode": true,
      "data": {
        "payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
        "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
        "amount": 250000,
        "fee": 1750,
        "fee_bearer": "merchant",
        "net_amount": 248250,
        "method": "QRIS",
        "channel": "QRIS",
        "paid_at": "2026-10-31T03:44:11.721+00:00",
        "metadata": {
          "order_id": "1042"
        }
      }
    }
  ]
}
GET/events/{id}#

Detail event & percobaan pengirimannya

Scope webhooks:read

Parameter

NamaDiTipeKeterangan
idwajibpathstring

id event (evt_ + 32 heksadesimal), mis. dari GET /events atau header ZePay-Event-Id.

Respons

  • 200

    OK

    Field respons (15)
    FieldTipeKeterangan
    idwajibstring

    evt_ + 32 heksadesimal

    typewajibstring
    payment.succeededpayment.expiredpayment.failedpayment.cancelledbalance.settledpayout.createdpayout.succeededpayout.failedpayout.cancelledbank_account.createdbank_account.verifiedbank_account.rejectedwebhook.test
    createdwajibstring (date-time)
    livemodewajibboolean
    datawajibobject
    delivery_statuswajibstring

    succeeded bila minimal satu endpoint menerima; failed bila semua pengiriman berhenti (dead); no_endpoint bila tidak ada endpoint yang berlangganan.

    pendingsucceededfailedno_endpoint
    deliverieswajibarray<WebhookDelivery>
    deliveries[].endpoint_idwajibstring (uuid)
    deliveries[].attemptwajibinteger

    Percobaan ke-n (naik setiap gagal).

    deliveries[].statuswajibstring

    pending menunggu / dijadwalkan ulang setelah gagal · succeeded diterima (2xx) · dead berhenti dicoba (±24 jam gagal beruntun atau endpoint nonaktif); kirim ulang dengan POST /events/{id}/resend.

    pendingsucceededfaileddead
    deliveries[].http_statuswajibinteger | null
    deliveries[].duration_mswajibinteger | null
    deliveries[].last_errorwajibstring | null
    deliveries[].next_attempt_atwajibstring (date-time) | null

    Hanya saat pending.

    deliveries[].created_atwajibstring (date-time)
  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X GET 'https://payment.envfri.com/api/v2/events/{id}' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 200

JSON
{
  "id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
  "type": "payment.succeeded",
  "created": "2026-10-31T10:44:12+07:00",
  "livemode": true,
  "data": {
    "payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
    "reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
    "amount": 250000,
    "fee": 1750,
    "fee_bearer": "merchant",
    "net_amount": 248250,
    "method": "QRIS",
    "channel": "QRIS",
    "paid_at": "2026-10-31T03:44:11.721+00:00",
    "metadata": {
      "order_id": "1042"
    }
  }
}
POST/events/{id}/resend#

Kirim ulang event ke endpoint aktif yang berlangganan

Scope webhooks:write

Membuat pengiriman baru (percobaan dari awal) ke setiap endpoint aktif yang berlangganan event ini. Tidak memakai Idempotency-Key.

Parameter

NamaDiTipeKeterangan
idwajibpathstring

id event (evt_ + 32 heksadesimal), mis. dari GET /events atau header ZePay-Event-Id.

Respons

  • 202

    Dijadwalkan

    Field respons (1)
    FieldTipeKeterangan
    scheduledwajibinteger

    Jumlah endpoint yang akan dikirimi.

  • 401

    authentication_failed, signature_invalid, atau timestamp_out_of_range — kunci, tanda tangan, atau jam tidak valid

  • 403

    permission_denied — kunci tidak punya scope yang dibutuhkan operasi ini (termasuk inquiry_id tanpa izin payouts:any_destination), atau IP pemanggil tidak ada di daftar IP kunci

  • 404

    not_found — tidak ditemukan, bukan milik merchant/mode ini, atau id tidak valid

  • 409

    no_webhook_endpoint — tidak ada endpoint aktif yang berlangganan event ini

  • 429

    rate_limited — terlalu banyak request; ulangi setelah Retry-After detik

Contoh request

cURL
curl -X POST 'https://payment.envfri.com/api/v2/events/{id}/resend' \
  -H 'X-ZePay-Key-Id: zpk_test_…' \
  -H 'X-ZePay-Timestamp: 1793446912' \
  -H 'X-ZePay-Signature: <hex HMAC-SHA256>'

Contoh respons 202

JSON
{
  "scheduled": 0
}