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 ¶
- Variables
- func ConvertStaticToDynamic(staticPayload string, amount string, opts ...ConvertOption) (string, error)
- func Validate(p *Payload) error
- type AdditionalData
- type Builder
- func (b *Builder) AddPSPAccount(tag, gui, mpan, mid, criteria string) *Builder
- func (b *Builder) AddQRISAccount(mpan, mid, criteria string) *Builder
- func (b *Builder) Build() (string, error)
- func (b *Builder) Country(country string) *Builder
- func (b *Builder) Currency(currency string) *Builder
- func (b *Builder) Dynamic(amount string) *Builder
- func (b *Builder) MCC(mcc string) *Builder
- func (b *Builder) MerchantCity(city string) *Builder
- func (b *Builder) MerchantName(name string) *Builder
- func (b *Builder) PostalCode(code string) *Builder
- func (b *Builder) SetAdditionalData(ad *AdditionalData) *Builder
- func (b *Builder) Static() *Builder
- type ConvertOption
- type CountryCode
- type CurrencyCode
- type FieldError
- type GUI
- type MerchantAccount
- type MerchantCriteria
- type Payload
- type ValidationError
Constants ¶
This section is empty.
Variables ¶
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.
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").
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 ¶
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 ¶
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 ¶
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 ¶
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) Dynamic ¶
Dynamic marks the payload as a dynamic QR (tag 01 = "12") carrying the given transaction amount (tag 54).
func (*Builder) MerchantCity ¶
MerchantCity sets tag 60. Required.
func (*Builder) MerchantName ¶
MerchantName sets tag 59. Required.
func (*Builder) PostalCode ¶
PostalCode sets tag 61. Optional.
func (*Builder) SetAdditionalData ¶
func (b *Builder) SetAdditionalData(ad *AdditionalData) *Builder
SetAdditionalData sets the tag 62 additional data block. Optional.
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 ¶
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 ¶
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) MPAN ¶
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.
Source Files
¶
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
|