Invoices through the API, paid notices by signed webhook.
A REST API v1 described in OpenAPI 3.1, Standard Webhooks to several endpoints, events you can read back for 30 days, and a test mode to try it all without real money.
One request, one invoice.
Send an amount; Konek QR adds the unique code and answers with the QR, a payment page link and the exact total to pay. Send the buyer to payUrl, or render qrString with any QR library.
| POST | /v1/paymentsCreate an invoice. Idempotency-Key required. | payments:write |
|---|---|---|
| GET | /v1/payments/{id}Invoice status, plus qrString while pending. | payments:read |
| GET | /v1/paymentsNewest first; filter by status and referenceId. | payments:read |
| POST | /v1/payments/{id}/cancelCancel a pending invoice. | payments:write |
| POST | /v1/payments/{id}/simulateMark as paid, test mode only. | payments:write |
| GET | /v1/qrisUsable QRIS, with their NMID. | payments:read |
| GET | /v1/eventsEvents after a cursor, long-polling up to 30 s. | events:read |
| GET | /v1/devicesListener phones and whether they are online. | payments:read |
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}'{
"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"
}
}
}Signed webhooks, to several servers.
- Standard Webhooks format: webhook-id, webhook-timestamp and webhook-signature (HMAC-SHA256).
- Up to 5 endpoints per account, each with its own secret and event filter. Rotating a secret keeps the old one valid for 24 hours.
- HTTPS only. Redirects are not followed, private addresses are refused.
- Answer 2xx within 15 seconds. Failures are retried for about 3 days: 1 minute, 5 minutes, 30 minutes, 2 hours and so on.
- Events: payment.paid, payment.expired, payment.cancelled, payment.unmatched, device.offline, device.online, topup.paid.
Webhook Doctor
- 403Add a WAF skip rule for the KonekQR-Webhooks user agent
cloudflare_challenge - 419Exclude the webhook route from Laravel’s VerifyCsrfToken
csrf_rejected - 3xxUse the final URL; redirects are not followed
redirect - TLSFix your server’s HTTPS certificate
tls_error - 15 sAnswer first, process afterwards
timeout
Diagnoses appear in the dashboard delivery log, next to every failed attempt.
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… }}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');
}No public URL? You still get the news.
GET /v1/events returns the events after a cursor, oldest first. With wait=30 the request waits until something new happens, so you just call it in a loop. Events are kept for 30 days: a consumer that was down picks up from its last cursor.
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-…" }Try it all without real money.
Test keys
A kq_test_… key only sees and creates test-mode objects. Live and test keys are created separately in the dashboard, and shown only once.
QRs that cannot be paid
Test invoice QRs use the issuer ID.KONEKQR.TEST and the name KONEK QR TEST, so payer apps refuse them. Their payment page carries a TEST MODE banner.
Simulated payments
POST /v1/payments/{id}/simulate marks a test invoice paid and sends payment.paid exactly like a real payment.
Never uses the quota
Test transactions count towards neither quota nor fees, and have their own pending limit.
SDKs for Node.js and Laravel.
@konekqr/node for Node.js 20.3+ and konekqr/laravel for Laravel 12/13: create and read invoices, iterate events and verify webhooks. Install with npm install @konekqr/node or composer require konekqr/laravel.
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');
}use KonekQR\Laravel\Facades\KonekQR;
$payment = KonekQR::createPayment(
['amount' => 25000, 'referenceId' => 'INV-1001'],
idempotencyKey: 'order-1001',
);Safeguards you can switch on.
So your bot never ships goods for a payment that cannot be confirmed, or to the wrong QRIS.
requireDetectionThe invoice is refused (409 detection_offline) when no Listener phone was active in the last 7 minutes.
expectedNmidThe invoice is refused (409 nmid_mismatch) when the QRIS is not the NMID you expect. The API and webhooks always include the NMID and merchant name.
Idempotency-KeyResend the same request after a timeout and you get the same invoice, not a second one. Valid for 24 hours.
Keys are server-only
Requests carrying a browser Origin are refused (403 browser_not_allowed). Scopes per key: payments:read, payments:write, events:read. Limit: 120 requests a minute per key.
Errors a machine can read
Every error is RFC 9457 application/problem+json with a stable code. Send Accept-Language: en for English messages. Every error code.
The full documentation.
The OpenAPI 3.1 spec is also at api.konekqr.web.id/openapi.yaml.
Connect your QRIS and take your first payment today.
Sign up free, upload your static QRIS, then try it in test mode before you take real money.