Konek QR

Quick start: your first QRIS invoice through the API

Create a test invoice, simulate the payment and receive the news in 10 minutes.

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

  1. Your server creates an invoice with an amount, for example Rp25,000.
  2. Konek QR adds a unique code of Rp1–499 (a total of, say, Rp25,193) and makes a dynamic QRIS from your static QRIS.
  3. The buyer pays from any app. The money goes straight to your merchant account, not through Konek QR.
  4. The Konek QR Listener app on your phone reads the "payment received" notification, Konek QR matches the total, and the invoice becomes paid.
  5. Your server hears about it through a webhook, or reads it from GET /v1/events.

1. Set up your account

  1. Sign up at app.konekqr.web.id.
  2. 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.
  3. 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.
Shell
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

Shell
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:

JSON
{
  "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. Set returnUrl to 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-Key is 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 header Idempotency-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.
  • referenceId is 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

Shell
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)

Shell
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.

  1. Dashboard → Webhooks → add an endpoint: a public https URL, test mode (an endpoint receives live or test events), and the payment.paid event (or all of them).
  2. Save the whsec_… secret it shows you.
  3. On your server, verify the signature, then process the event. Full examples for PHP, Node, Python and Go: Verifying webhooks.
  4. Press "Send test event", then simulate the invoice from step 4.

The webhook body is an Event:

JSON
{ "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)

Shell
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

  1. 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.
  2. Create a live key (kq_live_…) and a live webhook endpoint.
  3. 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.
  4. Keep an eye on your Listener phones: GET /v1/devices or the device.offline event.

Errors

Every error is application/problem+json. Compare the code, not the text:

JSON
{ "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. …" }
StatuscodeMeaning
400idempotency_key_invalidthe Idempotency-Key header is empty or longer than 100 characters
400invalid_jsonthe body is not valid JSON, or it has a field we do not know
401invalid_api_keythe key is wrong, revoked, or missing
403insufficient_scopethe key lacks the permission (payments:read, payments:write, events:read)
403browser_not_allowedthe request came from a browser; API keys are for servers only
403test_mode_onlya simulation with a live key
404not_foundthe invoice or QRIS does not exist (or belongs to the other mode)
409qris_not_ready, too_many_pending, unique_codes_exhausted, reference_taken, nmid_mismatch, detection_offline, payment_not_pending, idempotency_conflict, idempotency_in_progresssee the descriptions in the OpenAPI file
422validation_faileda field is wrong; the details are in errors[] (field, code, message)
429rate_limitedmore 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):

JavaScript
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):

PHP
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].