qris

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 6 Imported by: 0

README

qris-go

A Go library for parsing, generating, validating, and converting QRIS payment payloads.

CI Go Reference Go Version

Why this exists

QRIS (Quick Response Code Indonesian Standard) is Indonesia's national QR payment standard. As of mid-2025 it serves around 57 million users and 39 million merchants, and it now supports cross-border payments across five countries, including Japan and South Korea, with further expansion across APEC economies underway. This library provides a dependency-free, spec-aligned toolkit for working with QRIS payloads in Go: decoding them, building them, validating them, and converting static QRs into dynamic ones at the point of sale.

Features

  • Parser for EMVCo Merchant Presented Mode TLV payloads with CRC-16/CCITT-FALSE verification
  • Merchant account info parsing for tags 26-51 (PSP-specific entries and national QRIS routing)
  • Additional data (tag 62) parsing
  • Builder API for generating valid QRIS payloads with automatic length and CRC computation
  • Validation layer aligned with the ASPI/EMVCo specification
  • ConvertStaticToDynamic for the acquirer point-of-sale use case
  • Typed constants for merchant criteria, currency, country, and globally unique identifiers
  • Standard library only, no external dependencies

Installation

go get github.com/ezha-payment/qris-go

Quick start: parsing

package main

import (
	"fmt"
	"log"

	qris "github.com/ezha-payment/qris-go"
)

func main() {
	p, err := qris.Parse(payload)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(p.MerchantName, p.MerchantCity)
	fmt.Println(p.MPAN(), p.MID())
	if p.IsStatic() {
		fmt.Println("static QR")
	}
}

Usage: building a payload

The Builder computes every TLV length prefix and the trailing CRC automatically. Typed constants are passed with an explicit string conversion.

payload, err := qris.NewBuilder().
	Static().
	MerchantName("WARUNG SAMPLE").
	MerchantCity("JAKARTA").
	MCC("4812").
	Currency(string(qris.CurrencyIDR)).
	Country(string(qris.CountryID)).
	AddQRISAccount("9360091500001234567", "ID1020012345678", string(qris.CriteriaUMI)).
	Build()
if err != nil {
	log.Fatal(err)
}
fmt.Println(payload)

Build validates the payload before returning; an invalid configuration yields a *qris.ValidationError listing every offending field.

Usage: converting static to dynamic

A merchant displays a static QR; at checkout the point-of-sale embeds the purchase amount and emits a dynamic payload. Every original field is preserved; only the point of initiation method, amount, and optional additional data change.

dynamic, err := qris.ConvertStaticToDynamic(staticPayload, "25000",
	qris.WithReferenceLabel("INV-2024-001"),
)
if err != nil {
	log.Fatal(err)
}
fmt.Println(dynamic)

Runnable versions of these examples live in examples/.

API overview

Full reference: pkg.go.dev/github.com/ezha-payment/qris-go.

Symbol Purpose
Parse(string) (*Payload, error) Decode a QRIS payload, verifying its CRC.
Payload Parsed payload with typed accessors (MPAN, MID, IsStatic, IsDynamic, QRISMerchantAccount, PSPMerchantAccounts).
NewBuilder() *Builder Fluent builder for generating payloads.
Validate(*Payload) error Strict ASPI/EMVCo validation, returning a *ValidationError.
ConvertStaticToDynamic(payload, amount string, ...ConvertOption) Embed an amount into a static QR.
WithReferenceLabel, WithTerminalLabel, WithStoreLabel Additional-data overrides for conversion.
MerchantCriteria, CurrencyCode, CountryCode, GUI Typed constants for enum-like fields.

Specification references

  • ASPI QRIS Specification
  • EMVCo Merchant Presented Mode (MPM) Specification v1.1
  • Bank Indonesia PADG No. 21/18/PADG/2019

Disclaimer

This library implements public specifications. Not affiliated with Bank Indonesia or ASPI. Not a substitute for official certification.

Contributing

Contributions are welcome. See CONTRIBUTING.md for how to run tests, code style, and the pull request process.

License

Released under the MIT License.

Documentation

Overview

Package qris provides parsing, generation, validation, and conversion of QRIS payment payloads.

QRIS (Quick Response Code Indonesian Standard) is Indonesia's national QR payment standard, compliant with the EMVCo Merchant Presented Mode (MPM) specification. Payloads are ASCII TLV-encoded (Tag-Length-Value) strings terminated by a CRC-16/CCITT-FALSE checksum.

Features

  • Parser with CRC verification for EMVCo MPM TLV payloads.
  • Merchant account info parsing for tags 26-51 (PSP entries and the national QRIS routing identity in tag 51).
  • Additional data (tag 62) parsing.
  • Builder for generating payloads with automatic length and CRC computation.
  • Strict validation aligned with the ASPI/EMVCo specification.
  • ConvertStaticToDynamic for the acquirer point-of-sale use case.
  • Typed constants for merchant criteria, currency, country, and GUI.

The library depends only on the Go standard library.

Parsing

p, err := qris.Parse(payload)
if err != nil {
	// payload was malformed or its CRC did not verify
}
fmt.Println(p.MerchantName, p.MPAN())

The parser is intentionally permissive so it can decode real-world payloads. Use Validate to enforce the stricter rules required to emit a conformant payload.

Building

payload, err := qris.NewBuilder().
	Static().
	MerchantName("WARUNG SAMPLE").
	MerchantCity("JAKARTA").
	MCC("4812").
	AddQRISAccount("9360091500001234567", "ID1020012345678", string(qris.CriteriaUMI)).
	Build()

Builder.Build validates the result and recomputes every TLV length prefix and the trailing CRC, so callers never set those manually.

Converting static to dynamic

dynamic, err := qris.ConvertStaticToDynamic(staticPayload, "25000",
	qris.WithReferenceLabel("INV-2024-001"),
)

ConvertStaticToDynamic preserves every original field and only changes the point of initiation method, the amount, and any requested additional-data fields.

Specification references

  • ASPI QRIS Specification
  • EMVCo Merchant Presented Mode (MPM) Specification v1.1
  • Bank Indonesia PADG No. 21/18/PADG/2019

This library implements public specifications. It is not affiliated with Bank Indonesia or ASPI, and is not a substitute for official certification.

The package-level documentation lives in doc.go.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrPayloadTooShort  = errors.New("qris: payload too short")
	ErrInvalidTLVFormat = errors.New("qris: invalid TLV format")
	ErrInvalidCRC       = errors.New("qris: CRC validation failed")
	ErrInvalidLength    = errors.New("qris: invalid length field")
	ErrMissingCRCTag    = errors.New("qris: missing CRC tag")
)

Errors returned during parsing.

View Source
var ErrAlreadyDynamic = errors.New("qris: payload is already dynamic")

ErrAlreadyDynamic is returned by ConvertStaticToDynamic when the input payload is already a dynamic QR (point of initiation method "12").

View Source
var ErrValidation = errors.New("qris: validation failed")

ErrValidation is the sentinel wrapped by every ValidationError, so callers can test errors.Is(err, ErrValidation) regardless of which fields failed.

Functions

func ConvertStaticToDynamic

func ConvertStaticToDynamic(staticPayload string, amount string, opts ...ConvertOption) (string, error)

ConvertStaticToDynamic turns a static QRIS payload into a dynamic one carrying the given transaction amount.

This is the acquirer point-of-sale use case: a merchant displays a static QR, the customer's purchase amount is known at checkout, and the POS embeds that amount into a fresh dynamic payload. Every field of the original is preserved; only the point of initiation method (tag 01 becomes "12"), the amount (tag 54), and any additional data overrides (tag 62) change. The CRC is recomputed.

The conversion runs Parser -> modify -> Builder internally and never manipulates the payload string directly. It returns:

  • a wrapped parse error if staticPayload cannot be decoded,
  • ErrAlreadyDynamic if the input is already a dynamic QR,
  • a wrapped *ValidationError if the original or the resulting payload (including the amount) is invalid.

func Validate

func Validate(p *Payload) error

Validate checks a Payload against ASPI/EMVCo-aligned rules and returns a *ValidationError aggregating every violation found, or nil if the Payload is valid.

Validate is intentionally stricter than Parse: Parse is permissive so it can decode real-world payloads, whereas Validate enforces the constraints required to emit a conformant payload. Build calls Validate automatically before computing the CRC.

Types

type AdditionalData

type AdditionalData struct {
	BillNumber           string // sub-tag 01
	MobileNumber         string // sub-tag 02
	StoreLabel           string // sub-tag 03
	LoyaltyNumber        string // sub-tag 04
	ReferenceLabel       string // sub-tag 05
	CustomerLabel        string // sub-tag 06
	TerminalLabel        string // sub-tag 07
	PurposeOfTransaction string // sub-tag 08

	// Raw holds any sub-tags not explicitly modeled.
	Raw map[string]string
}

AdditionalData represents the parsed content of tag 62.

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder constructs valid QRIS payload strings from struct input.

Build() always recomputes the CRC and every TLV length prefix, so callers never set those manually. A Builder is created with NewBuilder, configured through chainable setters, and finalized with Build:

payload, err := qris.NewBuilder().
	Static().
	MerchantName("WARUNG SAMPLE").
	MerchantCity("JAKARTA").
	MCC("4812").
	AddQRISAccount("9360091500001234567", "ID1020012345678", "UMI").
	Build()

The output is guaranteed to round-trip through Parse: parsing a Builder's output yields an equivalent Payload.

func NewBuilder

func NewBuilder() *Builder

NewBuilder returns a Builder pre-populated with the QRIS defaults for payload format indicator (tag 00), transaction currency (tag 53), and country code (tag 58). Any of these may be overridden by the corresponding setter.

func (*Builder) AddPSPAccount

func (b *Builder) AddPSPAccount(tag, gui, mpan, mid, criteria string) *Builder

AddPSPAccount adds a Payment Service Provider specific merchant account at the given tag (one of 26-51) with the given GUI. A payload needs at least one merchant account (this or AddQRISAccount).

func (*Builder) AddQRISAccount

func (b *Builder) AddQRISAccount(mpan, mid, criteria string) *Builder

AddQRISAccount adds the national QRIS routing entry as tag 51, using the standard GUI "ID.CO.QRIS.WWW". A payload needs at least one merchant account (this or AddPSPAccount).

func (*Builder) Build

func (b *Builder) Build() (string, error)

Build assembles the configured fields into a complete QRIS payload string, computing every TLV length prefix and the trailing CRC.

All required fields are validated up front: if any are missing, the returned error lists all of them rather than failing on the first.

func (*Builder) Country

func (b *Builder) Country(country string) *Builder

Country sets the country code (tag 58). Defaults to "ID".

func (*Builder) Currency

func (b *Builder) Currency(currency string) *Builder

Currency sets the transaction currency (tag 53). Defaults to "360".

func (*Builder) Dynamic

func (b *Builder) Dynamic(amount string) *Builder

Dynamic marks the payload as a dynamic QR (tag 01 = "12") carrying the given transaction amount (tag 54).

func (*Builder) MCC

func (b *Builder) MCC(mcc string) *Builder

MCC sets the Merchant Category Code (tag 52). Required.

func (*Builder) MerchantCity

func (b *Builder) MerchantCity(city string) *Builder

MerchantCity sets tag 60. Required.

func (*Builder) MerchantName

func (b *Builder) MerchantName(name string) *Builder

MerchantName sets tag 59. Required.

func (*Builder) PostalCode

func (b *Builder) PostalCode(code string) *Builder

PostalCode sets tag 61. Optional.

func (*Builder) SetAdditionalData

func (b *Builder) SetAdditionalData(ad *AdditionalData) *Builder

SetAdditionalData sets the tag 62 additional data block. Optional.

func (*Builder) Static

func (b *Builder) Static() *Builder

Static marks the payload as a static QR (tag 01 = "11"), i.e. one without an embedded amount. It clears any previously set amount.

type ConvertOption

type ConvertOption func(*convertConfig)

ConvertOption customizes a ConvertStaticToDynamic call.

func WithReferenceLabel

func WithReferenceLabel(ref string) ConvertOption

WithReferenceLabel sets the additional data reference label (tag 62, sub-tag 05) on the converted payload, overriding any value carried by the original.

func WithStoreLabel

func WithStoreLabel(store string) ConvertOption

WithStoreLabel sets the additional data store label (tag 62, sub-tag 03) on the converted payload, overriding any value carried by the original.

func WithTerminalLabel

func WithTerminalLabel(terminal string) ConvertOption

WithTerminalLabel sets the additional data terminal label (tag 62, sub-tag 07) on the converted payload, overriding any value carried by the original.

type CountryCode

type CountryCode string

CountryCode is an ISO 3166-1 alpha-2 country code carried in tag 58.

const (
	// CountryID is Indonesia, the QRIS default.
	CountryID CountryCode = "ID"
	// CountrySG is Singapore.
	CountrySG CountryCode = "SG"
	// CountryMY is Malaysia.
	CountryMY CountryCode = "MY"
	// CountryTH is Thailand.
	CountryTH CountryCode = "TH"
	// CountryJP is Japan.
	CountryJP CountryCode = "JP"
	// CountryKR is South Korea.
	CountryKR CountryCode = "KR"
)

func (CountryCode) Description

func (c CountryCode) Description() string

Description returns the human-readable name of the country, or "Unknown" if the value is not recognized.

type CurrencyCode

type CurrencyCode string

CurrencyCode is an ISO 4217 numeric currency code carried in tag 53.

const (
	// CurrencyIDR is Indonesian Rupiah, the QRIS default.
	CurrencyIDR CurrencyCode = "360"
	// CurrencyUSD is United States Dollar.
	CurrencyUSD CurrencyCode = "840"
	// CurrencySGD is Singapore Dollar.
	CurrencySGD CurrencyCode = "702"
	// CurrencyMYR is Malaysian Ringgit.
	CurrencyMYR CurrencyCode = "458"
	// CurrencyTHB is Thai Baht.
	CurrencyTHB CurrencyCode = "764"
	// CurrencyJPY is Japanese Yen.
	CurrencyJPY CurrencyCode = "392"
	// CurrencyKRW is South Korean Won.
	CurrencyKRW CurrencyCode = "410"
)

func (CurrencyCode) Description

func (c CurrencyCode) Description() string

Description returns the human-readable name of the currency, or "Unknown" if the value is not recognized.

type FieldError

type FieldError struct {
	// Field is the name of the offending field (e.g. "MerchantName").
	Field string
	// Reason explains why the value is invalid.
	Reason string
}

FieldError describes a single field-level validation failure.

func (*FieldError) Error

func (e *FieldError) Error() string

Error implements the error interface.

type GUI

type GUI string

GUI is a Globally Unique Identifier carried in sub-tag 00 of a merchant account block. It identifies the routing network (national QRIS) or the specific Payment Service Provider.

const (
	// GUIQRISNational is the national QRIS switching identifier, used in
	// tag 51.
	GUIQRISNational GUI = "ID.CO.QRIS.WWW"
	// GUIDana is the DANA wallet identifier.
	GUIDana GUI = "ID.DANA.WWW"
	// GUIShopeePay is the ShopeePay identifier.
	GUIShopeePay GUI = "ID.SHOPEEPAY.WWW"
	// GUIOVO is the OVO identifier.
	GUIOVO GUI = "ID.OVO.WWW"
	// GUILinkAja is the LinkAja identifier.
	GUILinkAja GUI = "ID.LINKAJA.WWW"
	// GUIGoPay is the GoPay (Gojek) identifier.
	GUIGoPay GUI = "ID.CO.GOJEK.WWW"
)

func (GUI) Description

func (g GUI) Description() string

Description returns the human-readable name of the GUI's network or provider, or "Unknown" if the value is not recognized.

type MerchantAccount

type MerchantAccount struct {
	// GloballyUniqueIdentifier is sub-tag 00 within the merchant
	// account info block. Examples:
	//   - "ID.CO.QRIS.WWW"   (national QRIS switching)
	//   - "ID.DANA.WWW"      (DANA wallet)
	//   - "ID.LINKAJA.WWW"   (LinkAja)
	//   - "ID.SHOPEEPAY.WWW" (ShopeePay)
	GloballyUniqueIdentifier string

	// MPAN is the Merchant Primary Account Number (sub-tag 01).
	// Equivalent role to card PAN in card payments.
	// Typically 16-19 digits.
	MPAN string

	// MID is the Merchant ID (sub-tag 02), assigned by the
	// acquirer or PSP. Different from MPAN.
	MID string

	// MerchantCriteria is sub-tag 03. Common values:
	//   - "UMI" : Usaha Mikro (Micro)
	//   - "UKE" : Usaha Kecil (Small)
	//   - "UME" : Usaha Menengah (Medium)
	//   - "UBE" : Usaha Besar (Large)
	//   - "URE" : Regular (non-classified)
	MerchantCriteria string

	// Raw holds any sub-tags not explicitly modeled above,
	// keyed by sub-tag string. Useful for forward-compatibility
	// with PSP-specific extensions.
	Raw map[string]string
}

MerchantAccount represents a single Merchant Account Information entry (one of tags 26-51).

type MerchantCriteria

type MerchantCriteria string

MerchantCriteria is the merchant size classification carried in sub-tag 03 of a merchant account block.

const (
	// CriteriaUMI is Usaha Mikro (micro enterprise).
	CriteriaUMI MerchantCriteria = "UMI"
	// CriteriaUKE is Usaha Kecil (small enterprise).
	CriteriaUKE MerchantCriteria = "UKE"
	// CriteriaUME is Usaha Menengah (medium enterprise).
	CriteriaUME MerchantCriteria = "UME"
	// CriteriaUBE is Usaha Besar (large enterprise).
	CriteriaUBE MerchantCriteria = "UBE"
	// CriteriaURE is Regular (non-classified).
	CriteriaURE MerchantCriteria = "URE"
)

func (MerchantCriteria) Description

func (c MerchantCriteria) Description() string

Description returns the human-readable name of the merchant criteria, or "Unknown" if the value is not recognized.

type Payload

type Payload struct {
	// PayloadFormatIndicator is tag 00. Always "01" for QRIS.
	PayloadFormatIndicator string

	// PointOfInitiationMethod is tag 01.
	// "11" = static QR (no amount), "12" = dynamic QR (with amount).
	PointOfInitiationMethod string

	// MerchantAccountInfo contains entries for tags 26 through 51.
	//
	// Tags 26-45 carry Payment Service Provider (PSP) specific
	// merchant data (e.g., DANA, GoPay, ShopeePay). Tag 51 carries
	// the national QRIS routing identity. A given payload may have
	// only 26 (or other 26-45 range), only 51, or both.
	//
	// The map key is the tag string ("26", "27", ..., "51") so
	// consumers can inspect exactly which slot was populated.
	MerchantAccountInfo map[string]MerchantAccount

	// MerchantCategoryCode is tag 52. ISO 18245 4-digit code.
	MerchantCategoryCode string

	// TransactionCurrency is tag 53. ISO 4217 numeric code.
	// "360" = Indonesian Rupiah (IDR).
	TransactionCurrency string

	// TransactionAmount is tag 54.
	// Empty for static QR, required for dynamic QR.
	TransactionAmount string

	// CountryCode is tag 58. ISO 3166-1 alpha-2 code.
	CountryCode string

	// MerchantName is tag 59. Maximum 25 characters.
	MerchantName string

	// MerchantCity is tag 60. Maximum 15 characters.
	MerchantCity string

	// PostalCode is tag 61. Optional.
	PostalCode string

	// AdditionalData contains parsed contents of tag 62.
	AdditionalData *AdditionalData

	// CRC is tag 63. 4-character uppercase hexadecimal
	// CRC-16/CCITT-FALSE checksum.
	CRC string
}

Payload represents a parsed QRIS payment payload.

QRIS payloads are TLV-encoded (Tag-Length-Value) strings ending with a CRC-16/CCITT-FALSE checksum.

func Parse

func Parse(payload string) (*Payload, error)

Parse decodes a QRIS payload string into a Payload struct.

The payload must end with the CRC tag "6304" followed by a 4-character hexadecimal CRC value. Returns an error if the payload is malformed or the CRC validation fails.

func (*Payload) IsDynamic

func (p *Payload) IsDynamic() bool

IsDynamic reports whether this is a dynamic QR (with amount).

func (*Payload) IsStatic

func (p *Payload) IsStatic() bool

IsStatic reports whether this is a static QR (no amount embedded).

func (*Payload) MID

func (p *Payload) MID() string

MID returns the Merchant ID using the same precedence as MPAN.

func (*Payload) MPAN

func (p *Payload) MPAN() string

MPAN returns the Merchant PAN from the QRIS national entry (tag 51) if available, falling back to the first PSP entry that has one. Returns empty string if no MPAN is found.

func (*Payload) PSPMerchantAccounts

func (p *Payload) PSPMerchantAccounts() []MerchantAccount

PSPMerchantAccounts returns all PSP-specific entries (tags 26-45). Order is not guaranteed.

func (*Payload) QRISMerchantAccount

func (p *Payload) QRISMerchantAccount() (MerchantAccount, bool)

QRISMerchantAccount returns the national QRIS routing entry (tag 51, with GUI "ID.CO.QRIS.WWW") if present.

This is the most useful entry for QRIS-compliant acquirer integration: MPAN and NMID flow through here.

type ValidationError

type ValidationError struct {
	Errors []*FieldError
}

ValidationError aggregates one or more FieldError values produced by Validate. It implements error, satisfies errors.Is(err, ErrValidation) via Is, and exposes each underlying FieldError to errors.As via Unwrap.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements the error interface, joining every field error.

func (*ValidationError) FieldErrors

func (e *ValidationError) FieldErrors() []*FieldError

FieldErrors returns the individual field errors.

func (*ValidationError) Is

func (e *ValidationError) Is(target error) bool

Is reports whether target is ErrValidation, enabling errors.Is(err, ErrValidation).

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() []error

Unwrap returns the individual field errors so that, for example, errors.As(err, &fe) with a *FieldError target succeeds.

Directories

Path Synopsis
cmd
gencrc command
gencrc reads a QRIS payload from stdin (with "XXXX" placeholder at the end for CRC), computes the correct CRC, and prints the fixed payload.
gencrc reads a QRIS payload from stdin (with "XXXX" placeholder at the end for CRC), computes the correct CRC, and prints the fixed payload.
gentestdata command
gentestdata generates valid QRIS testdata files programmatically using auto-computed length and CRC.
gentestdata generates valid QRIS testdata files programmatically using auto-computed length and CRC.
examples
convert command
Command convert turns a static QRIS payload into a dynamic one with an embedded amount, demonstrating qris.ConvertStaticToDynamic.
Command convert turns a static QRIS payload into a dynamic one with an embedded amount, demonstrating qris.ConvertStaticToDynamic.
parse command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL