Payload Format
You don't need to read this to use the package. It's here for anyone debugging why a particular banking app is behaving oddly, or building a decoder.
Structure
The payload is a tag-length-value string. Each field is a two-digit tag, a two-digit length, then the value.
0002 02 format version
0102 12 point of initiation
0202 00 payload type
0424 PK33ABCD0000... destination IBAN
0504 2500 amount (optional)
0712 311220262359 expiry (optional)
1004 A1B2 CRC-16 checksumThe Fields
| Tag | Length | Value |
|---|---|---|
00 | 2 | Format version, always 02 |
01 | 2 | 11 static, 12 dynamic |
02 | 2 | Payload type, always 00 |
04 | 24 | The Pakistani IBAN, normalised |
05 | 1–10 | Normalised amount, omitted when static |
07 | 12 | Expiry as DDMMYYYYHHMM, omitted when static |
10 | 4 | CRC-16 checksum |
Static vs Dynamic
Tag 01 is 12 when the payload carries an amount or an expiry, and 11 when it carries neither.
RaastQr::iban($iban)->toPayload();
// 000202 010211 020200 0424PK33...
RaastQr::iban($iban)->amount('2500')->toPayload();
// 000202 010212 020200 0424PK33... 05042500 0712...The Amount Field
The length is the actual character count, so it varies:
0501 4 amount of 4
0504 2500 amount of 2500
0507 5000.50 amount of 5000.50Normalisation strips commas and leading zeros, and rejects anything with more than two decimal places or more than ten characters. See Setting the Amount.
The Expiry Field
Always twelve characters, DDMMYYYYHHMM:
0712 311220262359 31 December 2026 at 23:59
0712 311220261830 31 December 2026 at 18:30A date supplied without a time is encoded at 23:59.
The Checksum
Tag 10 holds a CRC-16/CCITT-FALSE checksum:
- Polynomial
0x1021 - Initial value
0xFFFF - No reflection, no final XOR
- Rendered as four uppercase hexadecimal characters
The checksum covers everything before it including the literal 1004. That last detail catches people out when they write their own encoder.
$payload = '000202...0424PK33...' . '1004';
$payload .= Crc16::calculate($payload);Verifying by Hand
php artisan raast-qr:generate PK33ABCD0000000000000000 --amount=2500 --expiry=2026-12-31 --payload0002020102120202000424PK33ABCD000000000000000005042500071231122026235910040CDF
│ │ │ │ │ │ │
│ │ │ └─ IBAN, 24 chars │ │ └─ CRC
│ │ └─ payload type 00 │ └─ expiry 31/12/2026 23:59
│ └─ dynamic (12) └─ amount 2500
└─ format version 02What It Does Not Contain
- ❌ No account title — the payer's app resolves that from the IBAN
- ❌ No merchant identifier
- ❌ No payment reference or order number
- ❌ No currency code — PKR is implied
- ❌ No signature or authentication
That last point is worth sitting with. The payload is not signed. Anyone can construct one for any IBAN. That's fine, because a Raast QR can only ever send money to an account — it can't withdraw. But it does mean the payer's confirmation screen is the only real check in the system, which is why every screen you build should tell people to look at it.
Stability
The vector tests in tests/PayloadTest.php assert exact payload strings. If a change to the package makes one fail, the encoder has drifted from what banking apps read — that's a release blocker, not a test to update.
Made with ❤️ from Pakistan