Skip to Content
API ReferenceQR Decode

QR Decode API

Decode EMV-compliant merchant QR codes to extract payment information for seamless transactions.

Decode QR Code

Extract payment information from an EMV QR code string.

POST /business/decode-qr

Request Body

FieldTypeRequiredDescription
qrStringstringYesEMV QR code data string

Supported QR Formats

  • EMV Standard: Lipa na M-Pesa (Kenya)
  • TIPS: Tanzania Instant Payment System
  • TANQR: Tanzania QR Code Standard

All supported formats comply with EMVCo Merchant-Presented QR Code specifications.

Example Request - Kenya EMV QR

curl -X POST https://api.test.wakapay.io/business/decode-qr \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "qrString": "00020101021128370008ke.go.qr010801888880020504000053031045802KE5912KPLC PREPAID6006KITALE62470114KE123456789012280703LPMN0315017123456789012341630444F1" }'
const response = await fetch("https://api.test.wakapay.io/business/decode-qr", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ qrString: "00020101021128370008ke.go.qr010801888880020504000053031045802KE5912KPLC PREPAID6006KITALE62470114KE123456789012280703LPMN0315017123456789012341630444F1", }), }); const data = await response.json(); console.log(data);

Response

{ "amount": null, "channel": "Safaricom", "countryCode": "KE", "currency": "KES", "isStatic": true, "merchantName": "", "tillNumber": "01888880" }

Response Fields

| Field | Type | Description | | -------------- | ------- | ------------------------------------------------------------------------- | ------------------------------------------------- | | amount | number | null | Amount (null for static QR, value for dynamic QR) | | channel | string | Payment network (Safaricom, CRDB, Vodacom, etc.) | | countryCode | string | Country code (KE, TZ, etc.) | | currency | string | Currency code (KES, TZS, etc.) | | isStatic | boolean | true = static QR (amount not fixed), false = dynamic QR with fixed amount | | merchantName | string | Merchant name from QR code (may be empty) | | tillNumber | string | Till or merchant number extracted from QR |

QR Code Types

Static QR Codes

Static QR codes don’t have a pre-filled amount. Customers enter the amount at payment time.

{ "amount": null, "isStatic": true, "tillNumber": "01888880" }

Use case: Till numbers, merchant terminals where amounts vary per transaction.

Dynamic QR Codes

Dynamic QR codes contain a specific amount to be paid.

{ "amount": 1500, "isStatic": false, "tillNumber": "01888880" }

Use case: Invoices, bills, fixed-price items.

EMV QR Code Format

The test QR code decodes as follows:

QR String:

00020101021128370008ke.go.qr010801888880020504000053031045802KE5912KPLC PREPAID6006KITALE62470114KE123456789012280703LPMN0315017123456789012341630444F1

Breakdown:

  • 00020101 - Payload format indicator (EMV version)
  • 0211 - Point of initiation method
  • 2837... - Merchant account information (contains till number)
  • 5802KE - Country code (Kenya)
  • 5912KPLC PREPAID - Merchant name
  • 6006KITALE - Merchant city

Use Cases

Decode and Display Information

async function decodeAndDisplayQR(qrCodeData) { const response = await fetch( "https://api.test.wakapay.io/business/decode-qr", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ qrString: qrCodeData }), }, ); const data = await response.json(); // Display to user console.log("Channel:", data.channel); console.log("Till Number:", data.tillNumber); console.log("Country:", data.countryCode); console.log("Currency:", data.currency); if (data.isStatic) { console.log("Amount: Customer will enter amount"); } else { console.log("Fixed Amount:", data.amount, data.currency); } return data; }

Extract Till Number for Payment

async function payToQRCode(qrString, amount) { // Step 1: Decode QR code const decodeResponse = await fetch( "https://api.test.wakapay.io/business/decode-qr", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ qrString: qrString }), }, ); const qrData = await decodeResponse.json(); if (qrData.channel === "Safaricom" && qrData.countryCode === "KE") { // Step 2: Extract till number (remove leading zeros) const tillNumber = qrData.tillNumber.replace(/^0+/, ""); // Step 3: Use amount from QR if dynamic, otherwise use provided amount const paymentAmount = qrData.isStatic ? amount : qrData.amount; // Step 4: Verify the till number const verifyResponse = await fetch( "https://api.test.wakapay.io/business/verify-payment", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ countryCode: qrData.countryCode, type: "till", lipaNumber: tillNumber, }), }, ); const verifyData = await verifyResponse.json(); if (!verifyData.verified) { throw new Error("Till number verification failed"); } // Step 5: Process payment const paymentResponse = await fetch( "https://api.test.wakapay.io/business/payout/payment", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "till", senderCurrency: "USD", receiverCurrency: qrData.currency, amount: paymentAmount, receiverLipaNumber: tillNumber, payoutCountry: qrData.countryCode, businessReference: `QR-PAY-${Date.now()}`, // ... other required fields }), }, ); return await paymentResponse.json(); } throw new Error("Unsupported QR code type"); }

Validate QR Before Payment

async function validateQRCode(qrString) { try { const response = await fetch( "https://api.test.wakapay.io/business/decode-qr", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ qrString: qrString }), }, ); const data = await response.json(); return { valid: true, channel: data.channel, tillNumber: data.tillNumber, amount: data.amount, isStatic: data.isStatic, currency: data.currency, }; } catch (error) { return { valid: false, error: error.message, }; } }

Error Responses

422 - Invalid QR String

{ "code": 0, "error": "unsupported or malformed QR payload" }

Causes:

  • QR string is not valid EMV format
  • QR string is corrupted or incomplete
  • QR string is not from supported networks

422 - Missing qrString

{ "code": 0, "error": "unsupported or malformed QR payload" }

400 - Malformed JSON

{ "code": 0, "error": "invalid payload" }

401 - No Authorization

{ "message": "missing value in request header" }

Supported Payment Networks

CountryNetworkChannel NameQR Type
Kenya (KE)M-PesaSafaricomEMV
Tanzania (TZ)M-Pesa, Tigo, AirtelVariousTIPS/TANQR
Uganda (UG)MTN, AirtelVariousEMV
South Africa (ZA)VariousVariousEMV

Best Practices

  1. Validate before payment - Always decode and validate QR before processing payment
  2. Handle isStatic flag - Check if amount is included or needs to be entered
  3. Extract till number correctly - Remove leading zeros from tillNumber (e.g., “01888880” → “888880”)
  4. Verify after decoding - Use verify-payment endpoint to confirm till/paybill is valid
  5. Handle errors gracefully - Provide clear messages for invalid QR codes
  6. Show merchant info - Display channel and tillNumber to user for confirmation

Testing

TESTENV QR Code

Use this QR code for testing in the test environment:

00020101021128370008ke.go.qr010801888880020504000053031045802KE5912KPLC PREPAID6006KITALE62470114KE123456789012280703LPMN0315017123456789012341630444F1

Decoded result:

  • Till Number: 01888880 (use 888880 for payments)
  • Channel: Safaricom
  • Country: KE
  • Currency: KES
  • Static: true

After decoding, you can verify and pay to till 888880 using the verify-payment and payout/payment endpoints.

Last updated on