API Documentation

Semua yang kamu butuhkan untuk menerima pembayaran QRIS lewat notifikasi dompet: tiga endpoint, satu callback, dan satu makro di HP Android.

Base URLhttps://payku.lovable.app/api/public

Cara kerja dalam 4 langkah

Payku bukan acquirer. Uang tetap masuk langsung ke QRIS milikmu; Payku yang memastikan setiap pembayaran ketahuan milik invoice yang mana.

  1. 1

    Kamu buat invoice

    Server kamu memanggil POST /invoices dengan nominal. Payku menambahkan kode unik 1–999 (mis. Rp150.000 → Rp150.137) dan mengunci nominal itu supaya tidak dipakai invoice lain yang masih menunggu.

  2. 2

    Pembeli bayar QRIS

    Payku mengubah QRIS statis milikmu menjadi QRIS dinamis dengan nominal terkunci. Pembeli scan, nominalnya sudah pas, uang langsung masuk ke rekening/dompet kamu — bukan ke Payku.

  3. 3

    HP kamu meneruskan notifikasi

    Aplikasi seperti MacroDroid membaca notifikasi masuk dari DANA/GoPay/ShopeePay/m-banking dan mengirim teks mentahnya ke POST /notify.

  4. 4

    Payku mencocokkan & mengabari kamu

    Nominal pada notifikasi dicocokkan dengan invoice yang menunggu. Kalau cocok, invoice jadi PAID dan Payku mengirim callback bertanda tangan ke callback_url kamu.

Authentication

Semua endpoint di /api/public diautentikasi dengan API key merchant lewat header X-Api-Key. Key berformat pk_live_<prefix>_<rahasia> dan hanya ditampilkan sekali saat dibuat atau dirotasi — kami menyimpan hash SHA-256-nya saja, jadi kami sendiri tidak bisa membacanya kembali. Simpan di server, jangan di kode frontend.

headers
X-Api-Key: pk_live_a7c4q2m9_9f31a2c4d5e6...
Content-Type: application/json

Khusus /notify kamu juga bisa menambahkan header X-Payku-Signature berisi sha256=<hmac> dari body mentah memakai webhook_secret. Kalau header itu ada, Payku wajib memverifikasinya sebelum memproses — sangat disarankan agar API key yang bocor saja tidak cukup untuk memalsukan pembayaran.

Quickstart

Tiga panggilan ini sudah cukup untuk transaksi pertama kamu.

1. buat invoice
curl -X POST https://payku.lovable.app/api/public/invoices \
  -H "X-Api-Key: pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"amount": 150000, "wallet": "QRIS", "customer_name": "Rina"}'
2. arahkan pembeli ke payment_url
# Respons berisi payment_url dan qris_payload.
# Buka payment_url di browser pembeli, atau render qris_payload jadi QR sendiri.
https://payku.lovable.app/pay/INV-M9F2K1-A7C4Q
3. cek status (atau tunggu callback)
curl https://payku.lovable.app/api/public/invoices/INV-M9F2K1-A7C4Q -H "X-Api-Key: pk_live_..."

Endpoints

Semua respons berformat JSON. Nominal selalu integer rupiah tanpa desimal, waktu selalu ISO-8601 UTC.

POST
/invoices

Buat invoice. Payku mengunci nominal unik dan, untuk wallet QRIS, langsung mengembalikan payload QRIS dinamis siap ditampilkan.

Request
curl -X POST https://payku.lovable.app/api/public/invoices \
  -H "X-Api-Key: pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 150000,
    "wallet": "QRIS",
    "customer_name": "Rina",
    "description": "Order #5512",
    "expires_in_minutes": 60
  }'
Response · 201 Created
{
  "order_id": "INV-M9F2K1-A7C4Q",
  "amount": 150000,
  "unique_code": 137,
  "total_amount": 150137,
  "wallet": "QRIS",
  "status": "PENDING",
  "expires_at": "2026-03-19T09:41:00.000Z",
  "payment_url": "https://payku.lovable.app/pay/INV-M9F2K1-A7C4Q",
  "qris_payload": "00020101021226..."
}

Field body /invoices

FieldTipeKeterangan
amountintegerWajib. Nominal dasar dalam rupiah, 100 – 50.000.000. Tanpa desimal.
walletstringOpsional, default QRIS. Pilihan: QRIS, DANA, GOPAY, SHOPEEPAY.
customer_namestringOpsional, maks 80 karakter. Muncul di dasbor dan callback.
descriptionstringOpsional, maks 160 karakter. Catatan internal kamu.
expires_in_minutesintegerOpsional, default 60. Antara 5 dan 1440 menit.

Tagih pembeli sebesar total_amount, bukan amount. Selisih unique_code (maks Rp999) adalah kunci pencocokannya.

GET
/invoices/{order_id}

Baca status terkini satu invoice. Aman dipanggil berkala (polling) tiap 3–5 detik kalau kamu belum memasang callback.

Request
curl https://payku.lovable.app/api/public/invoices/INV-M9F2K1-A7C4Q \
  -H "X-Api-Key: pk_live_..."
Response · 200 OK
{
  "order_id": "INV-M9F2K1-A7C4Q",
  "amount": 150000,
  "unique_code": 137,
  "total_amount": 150137,
  "wallet": "QRIS",
  "customer_name": "Rina",
  "status": "PAID",
  "expires_at": "2026-03-19T09:41:00.000Z",
  "created_at": "2026-03-19T08:41:00.000Z",
  "paid_at": "2026-03-19T09:12:44.000Z",
  "paid_amount": 150137,
  "payment_url": "https://payku.lovable.app/pay/INV-M9F2K1-A7C4Q"
}
POST
/notify

Kirim teks notifikasi mentah dari HP kamu. Payku mem-parsing nominal & pengirimnya lalu mencocokkan ke invoice. Kirim idempotency_key agar percobaan ulang tidak dihitung dua kali.

Request
curl -X POST https://payku.lovable.app/api/public/notify \
  -H "X-Api-Key: pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Anda menerima Rp150.137 dari Rina via QRIS",
    "idempotency_key": "notif-88213"
  }'

# Opsional tapi disarankan — tanda tangani body mentahnya:
# X-Payku-Signature: sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | cut -d' ' -f2)
Response · 200 OK
{
  "status": "MATCHED",
  "message": "OK",
  "order_id": "INV-M9F2K1-A7C4Q",
  "amount": 150137,
  "wallet_type": "QRIS"
}

Field body /notify

FieldTipeKeterangan
textstringWajib, 3–4000 karakter. Teks notifikasi apa adanya.
amountintegerOpsional. Pakai kalau kamu sudah tahu nominalnya; menimpa hasil parsing.
wallet_typestringOpsional. DANA, GOPAY, SHOPEEPAY, atau QRIS.
senderstringOpsional. Nama pengirim, hanya untuk log.
referencestringOpsional. Nomor referensi bank, hanya untuk log.
idempotency_keystringSangat disarankan. ID notifikasi dari HP; percobaan ulang dengan key sama tidak diproses ulang.

Status & aturan pencocokan

Pencocokan memakai nominal persis. Satu invoice PENDING = satu total_amount unik per merchant (dijamin di level database), jadi tidak mungkin satu notifikasi cocok ke dua invoice.

Status invoiceArti
PENDINGInvoice dibuat, nominal unik terkunci, menunggu pembayaran.
PAIDNotifikasi cocok. Callback invoice.paid dikirim.
EXPIREDLewat expires_at tanpa pembayaran. Nominal uniknya dibebaskan lagi.
CANCELLEDDibatalkan manual dari dasbor selagi masih PENDING.
Hasil /notifyHTTPArti
MATCHED200Nominal cocok dengan satu invoice PENDING. Invoice jadi PAID.
IGNORED404Tidak ada invoice menunggu dengan nominal itu. Aman — biasanya transfer di luar Payku.
DUPLICATE200Notifikasi yang sama sudah pernah diproses, atau invoice sudah lunas. Tidak dihitung dua kali.
ERROR422Nominal tidak terbaca dari teks notifikasi. Kirim ulang dengan field amount.
  • Idempoten. Notifikasi yang sama (atau idempotency_key yang sama) hanya diproses sekali; sisanya tercatat DUPLICATE.
  • Anti bayar-ganda. Perubahan status ke PAID dijaga secara atomik — hanya invoice yang masih PENDING yang bisa dilunasi.
  • Kedaluwarsa otomatis. Invoice lewat expires_at jadi EXPIRED dan nominal uniknya dibebaskan untuk invoice berikutnya.
  • Semua tercatat. Setiap notifikasi masuk — cocok maupun tidak — tersimpan di tab Log beserta teks mentah, hasil parsing, dan alasannya.

Callback signature

Isi callback_url di dasbor tab API, lalu Payku mengirim POST ke sana setiap invoice lunas. Kalau server kamu tidak membalas 2xx, Payku mencoba ulang sampai 3 kali dengan jeda bertambah, dan setiap percobaan tercatat.

callback yang kami kirim
POST https://tokomu.com/webhook/payku
X-Payku-Event: invoice.paid
X-Payku-Delivery: INV-M9F2K1-A7C4Q
X-Payku-Timestamp: 2026-03-19T09:12:44.120Z
X-Payku-Attempt: 1
X-Payku-Signature: sha256=8f14e45fceea167a5a36dedd4bea2543...
Content-Type: application/json

{
  "event": "invoice.paid",
  "merchant_id": "mch_8f31a2c4d5e6",
  "order_id": "INV-M9F2K1-A7C4Q",
  "base_amount": 150000,
  "unique_code": 137,
  "total_amount": 150137,
  "wallet_type": "QRIS",
  "customer_name": "Rina",
  "paid_at": "2026-03-19T09:12:44.000Z"
}

Verifikasi tanda tangannya sebelum memproses: hitung ulang HMAC-SHA256 dari body mentah (jangan hasil JSON.parse lalu di-stringify ulang) memakai webhook_secret kamu, lalu bandingkan dengan fungsi timing-safe.

verifikasi (Node.js)
import crypto from "node:crypto";

export function verifyPaykuSignature(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(header ?? "");
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook/payku", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyPaykuSignature(req.body.toString(), req.get("X-Payku-Signature"), SECRET)) {
    return res.status(401).send("bad signature");
  }
  const event = JSON.parse(req.body.toString());
  // Idempoten: abaikan kalau order_id ini sudah pernah kamu tandai lunas.
  markOrderPaid(event.order_id, event.total_amount);
  res.sendStatus(200);   // balas 2xx cepat, proses berat di background
});
verifikasi (PHP)
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_PAYKU_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit('bad signature');
}
$event = json_decode($raw, true);
http_response_code(200);

Device setup (MacroDroid)

Forward wallet notifications from your Android phone to your webhook URL. Free MacroDroid supports up to five macros.

  1. 1Unggah QRIS statis kamu di dasbor → tab QRIS. Payku membaca merchant name, kota, dan NMID-nya.
  2. 2Salin API key di tab API. Key hanya tampil sekali karena kami menyimpan hash-nya.
  3. 3Pasang MacroDroid di HP Android kamu, izinkan akses notifikasi, dan pastikan Optimasi Baterai dimatikan (Unrestricted).
  4. 4Kunci aplikasi MacroDroid di 'Recent Apps' HP kamu agar tidak tertutup saat Clear RAM.
  5. 5Trigger: Notification → Notification Received. Pilih aplikasi dompet digital kamu.
  6. 6Action 1: Set Variable. Buat variabel baru bernama `notif_text` dan isi dengan nilai Magic Text `[notification]`.
  7. 7Action 2: JavaScript. Jalankan script generator Payku (tersedia di Dasbor > Integrasi Web) untuk membuat tanda tangan HMAC (X-Payku-Signature).
  8. 8Action 3: HTTP Request. Method POST ke endpoint Payku. Isi form menggunakan Magic Text MacroDroid: `[v=payku_body]`, `[v=payku_ts]`, dan `[v=payku_sig]`.
  9. 9Simpan, aktifkan makro, lalu uji lewat dasbor → tab Uji sebelum dipakai produksi.

Tips: matikan mode hemat baterai untuk aplikasi makro, dan jangan bersihkan notifikasi secara otomatis sebelum makronya sempat jalan.

Error codes

Semua error berbentuk { \u0022error\u0022: \u0022...\u0022 } dan, untuk kegagalan validasi, ditambah field details.

HTTPArti
400Body bukan JSON yang valid.
401Header X-Api-Key hilang/salah, atau tanda tangan HMAC tidak cocok.
404Invoice tidak ditemukan, atau tidak ada invoice yang cocok nominalnya.
409Kode unik habis untuk nominal itu, atau merchant belum mengunggah QRIS.
413Payload notifikasi lebih dari 8 KB.
422Body gagal validasi, atau nominal tidak terbaca dari notifikasi.
500Kesalahan tak terduga di sisi Payku. Aman untuk dicoba ulang.

FAQ

Kenapa nominalnya ditambah kode unik?

Notifikasi dompet tidak memuat order id — hanya nominal dan nama pengirim. Dengan mengunci nominal yang berbeda untuk tiap invoice menunggu, satu notifikasi hanya bisa cocok ke satu invoice. Ini yang membuat rekonsiliasi tetap akurat tanpa integrasi acquirer.

Bagaimana kalau pembeli bayar dua kali?

Notifikasi kedua akan berstatus DUPLICATE dan invoice tidak dibayar dua kali. Kelebihan dana tetap ada di dompet kamu dan bisa direfund manual.

Apakah uang lewat Payku?

Tidak. Pembayaran langsung ke QRIS/dompet milikmu. Payku hanya membaca notifikasi dan mencocokkannya, jadi tidak ada settlement atau dana mengendap.

Berapa lama jendela pencocokan?

Invoice dicocokkan selama masih PENDING dan belum lewat expires_at, maksimal 24 jam sejak dibuat.