In 10 minutes you create a test invoice, mark it paid with a simulation, and receive the news. Every example uses curl; the Node and Laravel SDKs are at the end.
Full reference: openapi.yaml (also at https://api.konekqr.web.id/openapi.yaml).
How it works, in short
- Your server creates an invoice with an amount, for example Rp25,000.
- Konek QR adds a unique code of Rp1–499 (a total of, say, Rp25,193) and makes a dynamic QRIS from your static QRIS.
- The buyer pays from any app. The money goes straight to your merchant account, not through Konek QR.
- The Konek QR Listener app on your phone reads the "payment received" notification, Konek QR matches the total, and the invoice becomes
paid. - Your server hears about it through a webhook, or reads it from
GET /v1/events.
1. Set up your account
- Sign up at app.konekqr.web.id.
- QRIS → add the static QRIS from your merchant app (upload the image or paste its text). For test mode, the QRIS does not need to be verified yet.
- API → create a test mode key. The key (
kq_test_…) is shown only once; keep it in a server environment variable, never in your code or in the browser.
export KONEKQR_KEY='kq_test_...'A test key only sees and creates test mode objects. A test invoice's QR cannot be paid on purpose (its issuer is ID.KONEKQR.TEST, its merchant KONEK QR TEST), so it is safe to try.
2. Create an invoice
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, "description": "2x iced coffee", "referenceId": "INV-1001", "expiresInMinutes": 15}'The 201 response:
{
"payment": {
"id": "01972d1c-4b8e-7c3a-9d2f-1a2b3c4d5e6f",
"kind": "invoice",
"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-4b8e-7c3a-9d2f-1a2b3c4d5e6f",
"qrString": "00020101021226…6304ABCD",
"qris": { "id": "…", "label": "Budi's Shop", "provider": "shopeepay", "merchantName": "EXAMPLE SHOP", "nmid": "ID0000000000000" },
"…": "…"
}
}Then pick one:
- Send the buyer to
payUrl: a ready-made payment page with the QR, a countdown, a live status, and a "Paid to: name · NMID · provider" line. SetreturnUrlto get a button back to your shop. - Show the QR yourself from
qrString(any QR library will do), and show the total (amount, Rp25,193) clearly. The buyer must pay exactly that total.
Things to remember:
Idempotency-Keyis required. Send the same request again with the same key (after a timeout, for example) and you get the same invoice, not a second one; the response carries the headerIdempotency-Replayed: true. Use your order id. A key lasts 24 hours.- Without
qrisId, the invoice uses your oldest usable QRIS. To list your QRIS:GET /v1/qris. referenceIdis unique per account and mode (409 reference_taken).metadata(a JSON object of up to 4 KB) comes back as is on every invoice and event.
3. Check the status
curl https://api.konekqr.web.id/v1/payments/01972d1c-4b8e-7c3a-9d2f-1a2b3c4d5e6f \
-H "Authorization: Bearer $KONEKQR_KEY"status: pending, paid, expired (past expiresAt), or cancelled. paidLate: true means it was paid after it expired: payments up to 24 hours after expiresAt are still matched, and you decide whether to go ahead with that order.
List invoices: GET /v1/payments?status=paid&limit=50 (newest first; get the next page with before=<last id>). Find one order: GET /v1/payments?referenceId=INV-1001. Cancel: POST /v1/payments/{id}/cancel.
4. Simulate a payment (test mode)
curl -X POST https://api.konekqr.web.id/v1/payments/01972d1c-4b8e-7c3a-9d2f-1a2b3c4d5e6f/simulate \
-H "Authorization: Bearer $KONEKQR_KEY"The invoice becomes paid with paidVia: "simulation", and a payment.paid event is sent exactly as for a real payment. In the dashboard, test invoices have the same "Simulate payment" button.
5. Receive the payment news
There are two ways; many integrations use both.
Webhook (recommended)
- Dashboard → Webhooks → add an endpoint: a public https URL, test mode (an endpoint receives live or test events), and the
payment.paidevent (or all of them). - Save the
whsec_…secret it shows you. - On your server, verify the signature, then process the event. Full examples for PHP, Node, Python and Go: Verifying webhooks.
- Press "Send test event", then simulate the invoice from step 4.
The webhook body is an Event:
{ "id": "evt_01972d1f-…", "type": "payment.paid", "livemode": false, "createdAt": "2026-11-04T09:31:12Z", "data": { "…invoice…": "…" } }Answer with a 2xx within 15 seconds. If delivery fails, Konek QR keeps retrying for about 3 days, and the delivery log shows a Webhook Doctor diagnosis.
Event API (no public URL needed, or as a backup)
curl "https://api.konekqr.web.id/v1/events?types=payment.paid&wait=30" \
-H "Authorization: Bearer $KONEKQR_KEY"The response: { "events": [...], "next": "evt_…" }. Save next and send it as after= on your next call; next moves forward even when the events it skips do not match types. With wait=30 the request waits until a new event arrives (long polling), so you can simply call it in a loop. Events are kept for 30 days: a consumer that was down just carries on from its last cursor.
The events: payment.paid, payment.expired, payment.cancelled, payment.unmatched (a payment that matches no invoice, for example a buyer who paid the static QR directly), device.offline, device.online.
6. Go live
- Verify your QRIS: set up Listener (Setting up Konek QR Listener), then in Dashboard → QRIS press verify and pay the small amount it asks for to your own QRIS. Once Listener detects it, the QRIS becomes
active. A second QRIS, and any after it, waits 24 hours after it is verified. - Create a live key (
kq_live_…) and a live webhook endpoint. - Consider two safety options on
POST /v1/payments:"requireDetection": true: the invoice is refused (409 detection_offline) if no Listener phone has been active in the last 7 minutes, so you do not sell goods whose payment cannot be confirmed."expectedNmid": "ID10…": the invoice is refused (409 nmid_mismatch) if its QRIS does not have this NMID.
- Keep an eye on your Listener phones:
GET /v1/devicesor thedevice.offlineevent.
Errors
Every error is application/problem+json. Compare the code, not the text:
{ "type": "https://konekqr.web.id/errors/invalid_api_key", "title": "Invalid API key", "status": 401, "code": "invalid_api_key", "detail": "The API key is missing, wrong or revoked. …" }| Status | code | Meaning |
|---|---|---|
| 400 | idempotency_key_invalid | the Idempotency-Key header is empty or longer than 100 characters |
| 400 | invalid_json | the body is not valid JSON, or it has a field we do not know |
| 401 | invalid_api_key | the key is wrong, revoked, or missing |
| 403 | insufficient_scope | the key lacks the permission (payments:read, payments:write, events:read) |
| 403 | browser_not_allowed | the request came from a browser; API keys are for servers only |
| 403 | test_mode_only | a simulation with a live key |
| 404 | not_found | the invoice or QRIS does not exist (or belongs to the other mode) |
| 409 | qris_not_ready, too_many_pending, unique_codes_exhausted, reference_taken, nmid_mismatch, detection_offline, payment_not_pending, idempotency_conflict, idempotency_in_progress | see the descriptions in the OpenAPI file |
| 422 | validation_failed | a field is wrong; the details are in errors[] (field, code, message) |
| 429 | rate_limited | more than 120 requests per minute per key; wait as long as Retry-After says |
detail and message are in Indonesian; send Accept-Language: en to get English.
SDK
Node.js 20.3+ (npm install @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 12/13 (composer require konekqr/laravel):
use KonekQR\Laravel\Facades\KonekQR;
$payment = KonekQR::createPayment(['amount' => 25000, 'referenceId' => 'INV-1001'], idempotencyKey: 'order-1001');Installation details are in each SDK's README.
Something unclear or wrong? Write to [email protected].