Dokumentasi API
DaftarMasuk

Dokumentasi API Pembayaran

Terima pembayaran lewat Virtual Account dan QRIS dengan satu antarmuka. Seluruh contoh pada halaman ini dapat langsung dicoba memakai kunci sandbox tanpa melibatkan dana sungguhan.

Mulai cepat

Tiga langkah sampai pembayaran pertama Anda terbentuk.

  1. Daftar dan terbitkan kunci sandbox dari halaman ringkasan.
  2. Kirim permintaan pembuatan pembayaran seperti contoh di bawah.
  3. Daftarkan alamat notifikasi agar sistem Anda tahu ketika pelanggan sudah membayar.
curl -X POST http://localhost:3040/api/v1/payments \
  -H "Authorization: Bearer lg_sandbox_kunci_anda" \
  -H "Idempotency-Key: PESANAN-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "PESANAN-0001",
    "amount": 150000,
    "method": { "type": "VIRTUAL_ACCOUNT", "channel": "BNI" }
  }'
Lingkungan sandbox
Kunci berawalan lg_sandbox_ memakai lingkungan uji penyedia pembayaran. Nomor Virtual Account yang terbentuk sungguhan, tetapi tidak ada dana yang berpindah.

Autentikasi

Setiap permintaan menyertakan kunci pada tajuk Authorization.

Authorization: Bearer lg_sandbox_a1b2c3d4_xxxxxxxxxxxxxxxx
Idempotency-Key: PESANAN-0001
Content-Type: application/json
AwalanLingkunganKeterangan
lg_sandbox_SandboxUntuk mencoba integrasi, tanpa dana sungguhan
lg_live_ProduksiMemproses dana sungguhan
Jaga kerahasiaan kunci
Simpan kunci di sisi peladen Anda. Jangan pernah menaruhnya pada kode yang berjalan di peramban atau aplikasi telepon, karena siapa pun dapat membacanya dari sana. Bila kunci bocor, cabut segera dari halaman ringkasan dan terbitkan yang baru.

Kanal pembayaran

GET/api/v1/channels

Daftar kanal yang benar-benar dapat Anda pakai, sudah dikelompokkan dan mengikuti lingkungan kunci yang dipakai. Gunakan ini untuk menyusun pilihan pembayaran, agar Anda tidak menawarkan kanal yang akan ditolak saat tagihan dibuat.

curl https://api.langsunggas.example/api/v1/channels \
  -H "Authorization: Bearer lg_sk_test_xxxxxxxx"
{
  "environment": "SANDBOX",
  "total": 9,
  "data": [
    {
      "group": "VIRTUAL_ACCOUNT",
      "group_name": "Virtual Account",
      "channels": [
        {
          "code": "BNI",
          "name": "Bank BNI",
          "method_type": "VIRTUAL_ACCOUNT",
          "description": "Virtual account nominal terkunci",
          "available": true
        }
      ]
    },
    {
      "group": "QR",
      "group_name": "QRIS",
      "channels": [
        {
          "code": "QRIS",
          "name": "QRIS",
          "method_type": "QRIS",
          "description": "Pindai kode dari aplikasi apa pun yang mendukung QRIS",
          "available": true
        }
      ]
    }
  ]
}
KelompokIsi
VIRTUAL_ACCOUNTTransfer ke nomor virtual account bank
QRQRIS
OVER_THE_COUNTERBayar tunai di minimarket
EWALLETE-Money
PAYLATERBayar nanti

Kolom code adalah nilai yang Anda kirim padamethod.channel, sedangkan method_typeadalah nilai untuk method.type.

Kanal dapat berubah sewaktu-waktu
Kanal dapat dimatikan oleh Anda sendiri pada halaman pengaturan, atau ditutup operator ketika bank sedang bermasalah. Ambil daftar ini secara berkala, jangan menyalinnya menjadi daftar tetap di sisi Anda. Tambahkan?include_disabled=true bila Anda perlu melihat kanal yang sedang mati beserta alasannya.

Membuat pembayaran

POST/api/v1/payments

Virtual Account:

{
  "reference_id": "PESANAN-2026-0001",
  "amount": 275000,
  "method": { "type": "VIRTUAL_ACCOUNT", "channel": "BNI" },
  "customer": { "name": "Budi Santoso" }
}

QRIS:

{
  "reference_id": "PESANAN-2026-0002",
  "amount": 45000,
  "method": { "type": "QRIS" }
}

Jawaban berhasil dengan kode 201:

{
  "id": "pay_xxxxxxxxxxxx",
  "reference_id": "PESANAN-2026-0001",
  "status": "PENDING",
  "amount": 275000,
  "currency": "IDR",
  "method": {
    "type": "VIRTUAL_ACCOUNT",
    "channel": "BNI",
    "accountNumber": "8808999979278987",
    "accountName": "XDT-InoPay"
  },
  "expires_at": "2026-08-21T03:59:21.317Z",
  "created_at": "2026-08-21T02:59:21.631Z"
}
KolomWajibKeterangan
reference_idYaNomor pesanan Anda. Harus unik untuk setiap tagihan.
amountYaNominal dalam rupiah penuh, tanpa desimal.
method.typeYaVIRTUAL_ACCOUNT atau QRIS.
method.channelUntuk VAKode bank dari GET /api/v1/channels, misalnya BNI, BRI, MANDIRI.
expires_atTidakWaktu kedaluwarsa. Bawaannya enam puluh menit.
customerTidakNama dan surel pelanggan.
metadataTidakData tambahan milik Anda, dikembalikan apa adanya.

Idempotensi

Tajuk Idempotency-Key wajib disertakan pada setiap pembuatan pembayaran. Tajuk ini mencegah tagihan ganda ketika jaringan bermasalah dan permintaan yang sama terkirim dua kali.

KeadaanYang terjadi
Kunci sama, isi samaJawaban pertama dikembalikan. Tidak ada pembayaran baru.
Kunci sama, isi berbedaDitolak dengan kode 409.
Kunci berbedaPembayaran baru dibuat.
Gunakan nomor pesanan Anda
Nilai yang baik adalah nomor pesanan itu sendiri, karena tetap sama pada setiap percobaan ulang. Nilai acak yang dibuat ulang setiap percobaan justru menghilangkan seluruh manfaatnya.

Membaca pembayaran

GET/api/v1/payments/{id}
GET/api/v1/payments?status=PAID&limit=50

Daftar memakai penomoran berbasis kursor. Sertakan nilainext_cursor dari jawaban sebelumnya untuk mengambil halaman berikutnya.

{
  "data": [
    {
      "id": "pay_xxxxxxxxxxxx",
      "status": "PAID",
      "amount": 275000,
      "paid_at": "2026-08-21T03:01:04Z"
    }
  ],
  "has_more": true,
  "next_cursor": "cmt2xxxxxxxxxx"
}

Notifikasi

Ketika status pembayaran berubah, kami mengirim pemberitahuan ke alamat yang Anda daftarkan pada halaman Notifikasi.

{
  "id": "evt_xxxxxxxxxxxx",
  "type": "payment.paid",
  "created_at": "2026-08-21T03:01:05Z",
  "livemode": true,
  "data": {
    "payment": {
      "id": "pay_xxxxxxxxxxxx",
      "reference_id": "PESANAN-2026-0001",
      "status": "PAID",
      "amount": 275000,
      "paid_at": "2026-08-21T03:01:04Z"
    }
  }
}
Periksa livemode
Alamat notifikasi didaftarkan terpisah untuk lingkungan uji dan sungguhan, dan masing-masing memiliki rahasianya sendiri. Kolomlivemode bernilai benar hanya pada pembayaran dengan dana sungguhan. Periksa kolom ini sebelum memenuhi pesanan, agar pembayaran uji tidak pernah dianggap lunas.
TajukIsi
X-Webhook-IdPenanda unik pengiriman
X-Webhook-TimestampWaktu penandatanganan dalam milidetik
X-Webhook-SignatureTanda tangan HMAC SHA-256
X-Webhook-AttemptNomor percobaan pengiriman

Pengiriman dianggap berhasil hanya bila sistem Anda menjawab dengan kode 2xx dalam sepuluh detik. Bila gagal, pengiriman diulang dengan jeda menaik: satu menit, lima menit, tiga puluh menit, dua jam, delapan jam, lalu dua puluh empat jam.

Notifikasi dapat tiba lebih dari sekali
Pastikan penanganan di sisi Anda aman terhadap pengulangan. Periksa apakah pesanan sudah pernah ditandai lunas sebelum memprosesnya lagi, agar satu pembayaran tidak terhitung dua kali.

Verifikasi tanda tangan

Wajib dilakukan
Tanpa pemeriksaan tanda tangan, siapa pun yang mengetahui alamat notifikasi Anda dapat mengirim pemberitahuan palsu dan membuat sistem menganggap pesanan sudah dibayar. Ini kesalahan integrasi paling sering terjadi dan paling merugikan.

Contoh dengan Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

// Ambil isi mentah, jangan diuraikan lebih dulu.
const rawBody = await request.text();
const timestamp = request.headers.get("x-webhook-timestamp");
const signature = request.headers.get("x-webhook-signature");

// Tolak notifikasi lama untuk mencegah pengiriman ulang.
if (Math.abs(Date.now() - Number(timestamp)) / 1000 > 300) {
  return new Response(null, { status: 400 });
}

const diharapkan = createHmac("sha256", RAHASIA_ANDA)
  .update(timestamp + "." + rawBody)
  .digest("hex");

const a = Buffer.from(diharapkan);
const b = Buffer.from(signature);

if (a.length !== b.length || timingSafeEqual(a, b) === false) {
  return new Response(null, { status: 400 });
}

const event = JSON.parse(rawBody);
return new Response(null, { status: 200 });

Contoh dengan PHP:

<?php
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];

if (abs(round(microtime(true) * 1000) - (int)$timestamp) / 1000 > 300) {
    http_response_code(400);
    exit;
}

$diharapkan = hash_hmac('sha256', $timestamp . '.' . $rawBody, RAHASIA_ANDA);

if (!hash_equals($diharapkan, $signature)) {
    http_response_code(400);
    exit;
}

$event = json_decode($rawBody, true);
http_response_code(200);
Tanda tangani isi mentah
Yang ditandatangani adalah isi mentah permintaan, bukan hasil penguraian JSON. Menguraikan lalu menyusun ulang JSON akan mengubah susunan karakter dan membuat tanda tangan tidak cocok. Uraikan JSON setelah tanda tangan terbukti sah.

Status pembayaran

CREATED  ->  PENDING  ->  PAID
                 |
                 +-->  EXPIRED / FAILED / CANCELLED
StatusArti
CREATEDTagihan dibuat, menunggu penyedia
PENDINGSiap dibayar pelanggan
PAIDSudah dibayar
EXPIREDMelewati batas waktu
FAILEDGagal dibuat
CANCELLEDDibatalkan
Status tidak pernah mundur
Pembayaran yang sudah lunas tidak akan kembali menjadi menunggu, dan yang sudah kedaluwarsa tidak dapat menjadi lunas.

Kode kesalahan

Seluruh kesalahan memakai bentuk yang sama.

{
  "error": {
    "code": "INVALID_AMOUNT",
    "message": "Nominal di luar batas yang diizinkan",
    "request_id": "req_xxxxxxxx",
    "details": []
  }
}
HTTPKodeArti
401UNAUTHORIZEDKunci tidak ada atau tidak sah
403FORBIDDENCakupan kunci tidak mencukupi
403MERCHANT_NOT_ACTIVEAkun belum aktif atau dihentikan
400VALIDATION_ERRORIsi permintaan tidak sesuai
400INVALID_AMOUNTNominal di luar batas
409IDEMPOTENCY_CONFLICTKunci idempotensi dipakai dengan isi berbeda
404RESOURCE_NOT_FOUNDData tidak ditemukan
429RATE_LIMITEDPermintaan terlalu sering
502PROVIDER_UNAVAILABLEPenyedia sedang bermasalah
502PROVIDER_STATE_UNKNOWNStatus belum dapat dipastikan
Menangani PROVIDER_STATE_UNKNOWN
Kode ini berarti permintaan mungkin sudah diproses penyedia, tetapi jawabannya tidak sampai kepada kami. Jangan membuat pembayaran baru dengan kunci idempotensi berbeda, karena berisiko menagih pelanggan dua kali. Kirim ulang permintaan yang sama dengan kunci idempotensi yang sama, atau periksa statusnya lewat daftar pembayaran.

Batas permintaan

JenisBatas
Membuat pembayaran60 per menit untuk setiap kunci
Membaca data300 per menit untuk setiap kunci

Melampaui batas menghasilkan kode 429. Tunggu sesuai tajukRetry-After sebelum mencoba lagi.

Praktik yang disarankan

Jangan percaya pengalihan halaman
Pengalihan halaman peramban setelah pembayaran bukan bukti bahwa pelanggan sudah membayar. Siapa pun dapat membuka alamat halaman sukses secara langsung. Jadikan notifikasi sebagai satu-satunya sumber kebenaran status pembayaran.
  • Simpan nilai id yang kami kembalikan, bukan hanya nomor pesanan Anda sendiri.
  • Sediakan pemeriksaan berkala sebagai cadangan bila notifikasi tidak kunjung tiba, misalnya karena peladen Anda sempat mati.
  • Catat setiap notifikasi yang masuk beserta hasil pemeriksaan tanda tangannya, agar penelusuran masalah lebih mudah.
  • Uji seluruh alur pada lingkungan sandbox, termasuk keadaan gagal, sebelum beralih ke produksi.