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-qrRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
qrString | string | Yes | EMV 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 PREPAID6006KITALE62470114KE123456789012280703LPMN0315017123456789012341630444F1Breakdown:
00020101- Payload format indicator (EMV version)0211- Point of initiation method2837...- Merchant account information (contains till number)5802KE- Country code (Kenya)5912KPLC PREPAID- Merchant name6006KITALE- 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
| Country | Network | Channel Name | QR Type |
|---|---|---|---|
| Kenya (KE) | M-Pesa | Safaricom | EMV |
| Tanzania (TZ) | M-Pesa, Tigo, Airtel | Various | TIPS/TANQR |
| Uganda (UG) | MTN, Airtel | Various | EMV |
| South Africa (ZA) | Various | Various | EMV |
Best Practices
- Validate before payment - Always decode and validate QR before processing payment
- Handle isStatic flag - Check if amount is included or needs to be entered
- Extract till number correctly - Remove leading zeros from tillNumber (e.g., “01888880” → “888880”)
- Verify after decoding - Use verify-payment endpoint to confirm till/paybill is valid
- Handle errors gracefully - Provide clear messages for invalid QR codes
- 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 PREPAID6006KITALE62470114KE123456789012280703LPMN0315017123456789012341630444F1Decoded result:
- Till Number:
01888880(use888880for 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.
Related
- Recipient Verification - Verify decoded till numbers
- Payouts - Payment - Pay to decoded till/paybill numbers
- Error Handling - Handle QR decode errors