Konek QR

Tagihan lewat API, kabar lunas lewat webhook bertanda tangan.

REST API v1 dengan OpenAPI 3.1, webhook Standard Webhooks ke beberapa endpoint, event yang bisa dibaca ulang 30 hari, dan mode uji untuk mencoba semuanya tanpa uang sungguhan.

Satu permintaan, satu tagihan.

Kirim nominal, Konek QR menambahkan kode unik dan membalas dengan QR, link halaman bayar, dan total yang harus dibayar. Arahkan pembeli ke payUrl, atau tampilkan qrString dengan library QR apa saja.

Endpoint REST API v1
POST/v1/paymentsBuat tagihan. Idempotency-Key wajib.payments:write
GET/v1/payments/{id}Status tagihan, plus qrString selama menunggu.payments:read
GET/v1/paymentsDaftar terbaru dulu; filter status dan referenceId.payments:read
POST/v1/payments/{id}/cancelBatalkan tagihan yang menunggu.payments:write
POST/v1/payments/{id}/simulateTandai lunas, hanya mode uji.payments:write
GET/v1/qrisQRIS yang bisa dipakai, dengan NMID.payments:read
GET/v1/eventsEvent setelah cursor, long-poll sampai 30 detik.events:read
GET/v1/devicesHP Listener dan status online-nya.payments:read
Buat tagihan uji
curl https://api.konekqr.web.id/v1/payments \
  -H "Authorization: Bearer $KONEKQR_KEY" \
  -H "Idempotency-Key: order-1001" \
  -H "Content-Type: application/json" \
  -d '{"amount": 25000, "referenceId": "INV-1001", "expiresInMinutes": 15}'
Contoh jawaban 201
{
  "payment": {
    "id": "01972d1c-4b8e-7c3a-9d2f-1a2b3c4d5e6f",
    "livemode": false,
    "status": "pending",
    "amount": 25193,
    "baseAmount": 25000,
    "uniqueCode": 193,
    "referenceId": "INV-1001",
    "expiresAt": "2026-11-04T09:45:00Z",
    "payUrl": "https://pay.konekqr.web.id/p/01972d1c-…",
    "qrString": "00020101021226…6304ABCD",
    "qris": {
      "provider": "shopeepay",
      "merchantName": "TOKO CONTOH",
      "nmid": "ID0000000000000"
    }
  }
}

Webhook bertanda tangan, ke beberapa server.

  • Format Standard Webhooks: webhook-id, webhook-timestamp, dan webhook-signature (HMAC-SHA256).
  • Sampai 5 endpoint per akun, masing-masing dengan secret dan filter event sendiri. Rotasi secret menjaga yang lama berlaku 24 jam.
  • Hanya HTTPS. Redirect tidak diikuti, alamat privat ditolak.
  • Jawab 2xx dalam 15 detik. Kalau gagal, dicoba lagi selama sekitar 3 hari: 1 menit, 5 menit, 30 menit, 2 jam, dan seterusnya.
  • Event: payment.paid, payment.expired, payment.cancelled, payment.unmatched, device.offline, device.online, topup.paid.

Dokter Webhook

  1. 403Buat WAF skip rule untuk User-Agent KonekQR-Webhookscloudflare_challenge
  2. 419Kecualikan route webhook dari VerifyCsrfToken Laravelcsrf_rejected
  3. 3xxPakai URL akhirnya; redirect tidak diikutiredirect
  4. TLSPerbaiki sertifikat HTTPS servermutls_error
  5. 15 sJawab dulu, proses belakangantimeout

Diagnosis muncul di log pengiriman dashboard, di samping setiap percobaan yang gagal.

Contoh kiriman webhook
POST /webhooks/konekqr HTTP/1.1
content-type: application/json
user-agent: KonekQR-Webhooks/1
webhook-id: evt_01972d1f-…
webhook-timestamp: 1793784672
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{"id":"evt_01972d1f-…","type":"payment.paid","livemode":false,
 "createdAt":"2026-11-04T09:31:12Z","data":{ …the invoice… }}
Verifikasi di Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyKonekQRWebhook(secret, headers, rawBody, now = Math.floor(Date.now() / 1000)) {
  const id = headers['webhook-id'] ?? '';
  const timestamp = headers['webhook-timestamp'] ?? '';
  if (!/^\d+$/.test(timestamp) || Math.abs(now - Number(timestamp)) > 300) throw new Error('stale');
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(rawBody).digest();
  for (const signature of (headers['webhook-signature'] ?? '').split(' ')) {
    const [version, value = ''] = signature.split(',', 2);
    const given = Buffer.from(value, 'base64');
    if (version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected)) {
      return JSON.parse(rawBody.toString());
    }
  }
  throw new Error('bad signature');
}

Contoh PHP, Python, Go, dan cURL

Tanpa URL publik pun tetap dapat kabar.

GET /v1/events mengembalikan event setelah cursor, terlama dulu. Dengan wait=30, permintaan menunggu sampai ada event baru, jadi cukup diulang terus. Event disimpan 30 hari: konsumen yang sempat mati tinggal melanjutkan dari cursor terakhirnya.

Long-poll event
curl "https://api.konekqr.web.id/v1/events?after=evt_01972d1f-…&types=payment.paid&wait=30" \
  -H "Authorization: Bearer $KONEKQR_KEY"

# → { "events": [ … ], "next": "evt_01972d2a-…" }

Coba semuanya tanpa uang sungguhan.

  • Key uji

    Key kq_test_… hanya melihat dan membuat objek mode uji. Key live dan uji dibuat terpisah di dashboard, dan hanya ditampilkan sekali.

  • QR yang tidak bisa dibayar

    QR tagihan uji memakai penerbit ID.KONEKQR.TEST dan nama KONEK QR TEST, jadi aplikasi pembayar menolaknya. Halaman bayarnya berlabel MODE UJI.

  • Simulasi lunas

    POST /v1/payments/{id}/simulate menandai tagihan uji lunas dan mengirim payment.paid persis seperti pembayaran sungguhan.

  • Tidak memakai kuota

    Transaksi uji tidak dihitung kuota maupun biaya, dan punya batas antrean sendiri.

SDK untuk Node.js dan Laravel.

@konekqr/node untuk Node.js 20.3+ dan konekqr/laravel untuk Laravel 12/13: buat dan baca tagihan, iterasi event, dan verifikasi webhook. Pasang dengan npm install @konekqr/node atau composer require konekqr/laravel.

Node.js: @konekqr/node
import { KonekQR } from '@konekqr/node';

const kq = new KonekQR({ apiKey: process.env.KONEKQR_KEY });
const payment = await kq.createPayment(
  { amount: 25000, referenceId: 'INV-1001' },
  { idempotencyKey: 'order-1001' },
);
for await (const event of kq.events({ types: ['payment.paid'] })) {
  console.log(event.data.referenceId, 'paid');
}
Laravel: konekqr/laravel
use KonekQR\Laravel\Facades\KonekQR;

$payment = KonekQR::createPayment(
    ['amount' => 25000, 'referenceId' => 'INV-1001'],
    idempotencyKey: 'order-1001',
);

Pengaman yang bisa kamu nyalakan.

Supaya botmu tidak mengirim barang untuk pembayaran yang tidak bisa dikonfirmasi, atau ke QRIS yang salah.

  • requireDetection

    Tagihan ditolak (409 detection_offline) kalau tidak ada HP Listener yang aktif dalam 7 menit terakhir.

  • expectedNmid

    Tagihan ditolak (409 nmid_mismatch) kalau QRIS-nya bukan NMID yang kamu harapkan. API dan webhook selalu menyertakan NMID dan nama merchant.

  • Idempotency-Key

    Kirim ulang permintaan yang sama saat timeout, dan kamu mendapat tagihan yang sama, bukan tagihan kedua. Berlaku 24 jam.

  • Key hanya untuk server

    Permintaan dengan Origin browser ditolak (403 browser_not_allowed). Scope per key: payments:read, payments:write, events:read. Batas 120 permintaan per menit per key.

  • Error yang bisa dibaca mesin

    Semua error berformat RFC 9457 application/problem+json dengan code yang stabil. Kirim Accept-Language: en untuk pesan berbahasa Inggris. Semua kode error.

Sambungkan QRIS kamu, terima pembayaran pertama hari ini.

Daftar gratis, unggah QRIS statis, lalu coba di mode uji sebelum menerima uang sungguhan.