ahr-ahr/qris-dynamic

PHP library for parsing and generating dynamic QRIS payloads.

Maintainers

Package info

github.com/ahr-ahr/qris-dynamic

pkg:composer/ahr-ahr/qris-dynamic

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-05-13 17:32 UTC

This package is auto-updated.

Last update: 2026-05-13 17:45:43 UTC


README

English | Bahasa Indonesia

QRIS Dynamic

PHP library for parsing, validating, decoding, and generating dynamic QRIS payloads.

PHP Tests License

Why This Library Exists

Most QRIS-related repositories only do one thing:

  • Inject amount using regex
  • Replace strings blindly
  • Ignore TLV structure
  • Ignore CRC validation
  • Fail on nested QRIS tags

This library was built differently.

qris-dynamic understands the actual QRIS payload structure using a recursive TLV parser, CRC16 validation, payload abstraction, and dynamic QRIS generation.

What is QRIS Internally?

QRIS payload is basically:

TAG + LENGTH + VALUE

Example:

540510000

Can be read as:

Part Meaning
54 Transaction Amount
05 Length
10000 Value

So mathematically:

TLV = T + L + V

Where:

T = Tag
L = Length
V = Value

QRIS Anatomy

Example QRIS payload:

00020101021126630016COM.EXAMPLE.PSP011212345678905204481253033605802ID5915SAMPLE MERCHANT6013MERCHANT CITY6304ABCD

Visual Breakdown

graph TD

QRIS[QRIS Payload]

QRIS --> T00[00 - Payload Format]
QRIS --> T01[01 - QR Type]
QRIS --> T26[26 - Merchant Info]
QRIS --> T52[52 - MCC]
QRIS --> T53[53 - Currency]
QRIS --> T54[54 - Amount]
QRIS --> T58[58 - Country]
QRIS --> T59[59 - Merchant Name]
QRIS --> T60[60 - City]
QRIS --> T63[63 - CRC]
Loading

Important QRIS Tags

Tag Meaning Example
00 Payload Format Indicator 01
01 QR Type 11 = Static
26 Merchant Account Info Payment Provider
52 Merchant Category Code 4812
53 Currency 360 = IDR
54 Transaction Amount 10000
58 Country Code ID
59 Merchant Name SAMPLE MERCHANT
60 Merchant City MERCHANT CITY
63 CRC16 Checksum ABCD

Static vs Dynamic QRIS

Static QRIS

Static QRIS usually does not include tag 54 (transaction amount).

The customer enters the amount manually during payment.

Dynamic QRIS

Dynamic QRIS contains transaction amount.

Example:

540510000

Can be interpreted as:

Part Meaning
54 Amount Tag
05 Length
10000 Amount

This library injects the amount automatically and recalculates CRC16 safely.

CRC16 Visualization

CRC is the integrity protection layer of QRIS.

If payload changes:

Merchant Name
Amount
City
Any Tag

then CRC becomes invalid.

Simplified CRC Flow

flowchart LR

A[Raw TLV Payload] --> B[CRC16 Calculation]
B --> C[Hex Checksum]
C --> D[Append to Tag 63]
Loading

Nested Merchant Information

Some QRIS tags contain nested TLV values.

Example:

26
 ├── 00 → COM.EXAMPLE.PSP
 ├── 01 → Merchant ID
 ├── 02 → Merchant Criteria
 └── 03 → UMI

This library recursively parses nested structures automatically.

TLV Structure Visualization

graph TD

A[54] --> B[05]
B --> C[10000]

A:::tag
B:::length
C:::value

classDef tag fill:#ffb703,color:#000;
classDef length fill:#8ecae6,color:#000;
classDef value fill:#90be6d,color:#000;
Loading

How Parsing Works

QRIS parsing is essentially a sequential string slicing process.

The parser reads:

TAG → LENGTH → VALUE

repeatedly until the payload ends.

Example

Payload:

540510000

Parser reads it like this:

flowchart LR

A["54"] --> B["05"]
B --> C["10000"]

A:::tag
B:::length
C:::value

classDef tag fill:#ffb703,color:#000;
classDef length fill:#8ecae6,color:#000;
classDef value fill:#90be6d,color:#000;
Loading

Mathematical Interpretation

If:

T = Tag
L = Length
V = Value

then:

TLV = T + L + V

and:

Length(V) = L

Example:

54 05 10000

means:

Length("10000") = 5

which is valid.

Sequential Parsing Algorithm

The parser works using offsets.

Pseudo flow:

1. Read 2 characters  → TAG
2. Read next 2 chars  → LENGTH
3. Read next N chars  → VALUE
4. Move offset
5. Repeat

Offset Visualization

flowchart LR

A["Read Tag<br>Offset 0"]
--> B["Read Length<br>Offset 2"]
--> C["Read Value<br>Offset 4"]
--> D["Move Offset"]
Loading

Parser Complexity

The TLV parser operates in linear time complexity.

Time Complexity: O(n)
Space Complexity: O(n) worst case

because the payload is scanned sequentially using offsets.

Nested TLV Parsing

Some QRIS tags contain another TLV structure internally.

Example:

26
 ├── 00 → Provider
 ├── 01 → Merchant ID
 └── 03 → Category

This means the parser must recursively parse child payloads.

Recursive Parsing Visualization

graph TD

A[Root Payload]
A --> B[Tag 26]
B --> C[Subtag 00]
B --> D[Subtag 01]
B --> E[Subtag 03]
Loading

Internal Parsing Flow

flowchart TD

A[QR Image]
--> B[Decode Image]
--> C[Extract Payload]
--> D[Parse TLV]
--> E[Build Payload Object]
--> F[Modify Tags]
--> G[Rebuild TLV]
--> H[Generate CRC16]
--> I[Dynamic QRIS]
Loading

Why Recursive Parsing Matters

Many QRIS implementations fail because merchant account information itself contains nested TLV payloads.

Naive parsers usually:

  • split incorrectly
  • fail on nested tags
  • break CRC generation
  • generate invalid QRIS payloads

This library recursively parses nested structures safely before rebuilding the payload and recalculating CRC16.

Features

  • Decode QRIS image
  • Parse TLV payloads
  • Recursive nested TLV parser
  • Generate dynamic QRIS
  • CRC16 generation & validation
  • Fluent API
  • Payload abstraction
  • QRIS validation
  • Exception hierarchy
  • PHPUnit tested

Installation

composer require ahr-ahr/qris-dynamic

Basic Usage

use AhrAhr\QRIS\QRIS;

$qris = QRIS::fromImage(
    'images/sample-qris.png'
);

$payload = $qris
    ->amount(10000)
    ->generate();

echo $payload;

Generate Dynamic QRIS

<?php

require 'vendor/autoload.php';

use AhrAhr\QRIS\QRIS;

$qris = QRIS::fromImage(
    'images/sample-qris.png'
);

$payload = $qris
    ->amount(25000)
    ->generate();

echo $payload;

Decode QRIS Image

<?php

use AhrAhr\QRIS\Decoder\QRImageDecoder;

$result = QRImageDecoder::decode(
    'images/sample-qris.png'
);

echo $result->payload;

Parse TLV Payload

<?php

use AhrAhr\QRIS\Parser\TLVParser;

$nodes = TLVParser::parse($payload);

foreach ($nodes as $node) {

    echo $node->tag . PHP_EOL;
    echo $node->value . PHP_EOL;
}

Example Output

[00] Payload Format Indicator
[01] Point of Initiation Method

[26] Merchant Account Information
  [00] Globally Unique Identifier
  [01] Merchant ID

[52] Merchant Category Code
[53] Transaction Currency
[54] Transaction Amount
[59] Merchant Name
[63] CRC

Validation Logic

QRIS validation in this library consists of two layers:

  1. Structural validation
  2. CRC16 checksum validation

Structural Validation

The parser verifies:

  • Required tags exist
  • TLV lengths are valid
  • Nested payloads are parsable

Example:

54 05 10000

is valid because:

Length("10000") = 5

CRC16 Validation

CRC16 ensures payload integrity.

If even one character changes:

Merchant Name
Amount
Country
Any Value

then checksum becomes invalid.

Validation Pipeline

flowchart LR

A[Raw Payload]
--> B[Parse TLV]
--> C[Validate Required Tags]
--> D[Validate CRC16]
--> E[Valid QRIS]
Loading

Validate QRIS

Quick Validation

use AhrAhr\QRIS\Parser\PayloadValidator;

$isValid = PayloadValidator::validate(
    $payload
);

var_dump($isValid);

Validation With Exceptions

use AhrAhr\QRIS\Exceptions\QRISException;
use AhrAhr\QRIS\Parser\PayloadValidator;

try {

    PayloadValidator::validateOrFail(
        $payload
    );

} catch (QRISException $e) {

    echo $e->getMessage();
}

Fluent API

$qris = QRIS::fromImage(
    'images/sample-qris.png'
);

$generated = $qris
    ->amount(10000)
    ->generate();

Exception Hierarchy

classDiagram

QRISException <|-- InvalidPayloadException
QRISException <|-- InvalidCRCException
QRISException <|-- UnsupportedTagException
Loading

Example Parsed QRIS

[00] Payload Format Indicator
[01] Point of Initiation Method

[26] Merchant Account Information
  [00] Globally Unique Identifier
  [01] Merchant ID
  [03] Merchant Category

[52] Merchant Category Code
[53] Transaction Currency
[54] Transaction Amount
[58] Country Code
[59] Merchant Name
[60] Merchant City
[63] CRC

Project Structure

qris-dynamic/
│
├── src/
│   ├── QRIS.php
│   │
│   ├── Contracts/
│   │   ├── ParserInterface.php
│   │   ├── GeneratorInterface.php
│   │   └── QRISInterface.php
│   │
│   ├── Decoder/
│   │   ├── QRImageDecoder.php
│   │   └── QRDecoderResult.php
│   │
│   ├── Parser/
│   │   ├── TLVParser.php
│   │   ├── TLVNode.php
│   │   ├── Payload.php
│   │   └── PayloadValidator.php
│   │
│   ├── Generator/
│   │   ├── DynamicQRISGenerator.php
│   │   ├── StaticQRISGenerator.php
│   │   └── CRC16.php
│   │
│   ├── ValueObjects/
│   │   ├── Amount.php
│   │   ├── MerchantInfo.php
│   │   ├── Tag.php
│   │   └── CRC.php
│   │
│   ├── Exceptions/
│   │   ├── InvalidCRCException.php
│   │   ├── InvalidPayloadException.php
│   │   ├── UnsupportedTagException.php
│   │   └── QRISException.php
│   │
│   └── Utils/
│       ├── StringHelper.php
│       └── TagHelper.php
│
├── examples/
│   ├── decode-image.php
│   ├── parse.php
│   ├── fluent-api.php
│   ├── generate-image.php
│   └── validate.php
│
├── images/
│   └── sample-qris.png
│
├── tests/
│
├── composer.json
├── phpunit.xml
├── README.md
└── LICENSE

Testing

Run all tests:

composer test

Current Test Status

25 tests
35 assertions
All passing

Roadmap

  • QRIS image exporting helper
  • Better merchant abstraction
  • Tag specification validation
  • Multi-provider QRIS utilities
  • Packagist release
  • GitHub Actions CI

License

This project is licensed under the MIT License.

Disclaimer

This library is an independent open-source project and is not affiliated with Bank Indonesia or any payment provider.

Use responsibly and always validate generated QRIS payloads before production use.