Themia.PromptPay 0.29.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Themia.PromptPay --version 0.29.0
                    
NuGet\Install-Package Themia.PromptPay -Version 0.29.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Themia.PromptPay" Version="0.29.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Themia.PromptPay" Version="0.29.0" />
                    
Directory.Packages.props
<PackageReference Include="Themia.PromptPay" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Themia.PromptPay --version 0.29.0
                    
#r "nuget: Themia.PromptPay, 0.29.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Themia.PromptPay@0.29.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Themia.PromptPay&version=0.29.0
                    
Install as a Cake Addin
#tool nuget:?package=Themia.PromptPay&version=0.29.0
                    
Install as a Cake Tool

Themia.PromptPay

PromptPay QR payload construction — EMVCo TLV assembly and CRC-16 for Credit Transfer (Tag 29) and Bill Payment (Tag 30).

Pure computation: no HTTP, no credentials, no clock, no I/O. Targets net8.0 and net10.0.

Rendering the payload as a QR image is not included — that pulls a drawing dependency into every consumer, and the payload is the part that has to be right.

Bill Payment (Tag 30)

The product a payment belongs to comes from the registration, not from the call:

// composition root, once per product
var biller = BillerRegistration.PerProductSuffix(taxId: "0105500000000", suffix: "01");

// every call site
var payload = PromptPayQr.BillPayment(biller, reference: "INV-00042", amount: 590m);

When two products bill under one Tax ID, they must be distinguishable. Where they are distinguished depends on what the bank issued:

The bank issued Use The discriminator lives in
One suffix per product PerProductSuffix(taxId, suffix) the Biller ID
One suffix for both SharedSuffix(taxId, suffix, productPrefix) a prefix on Reference 1

SharedSuffix cannot be constructed without a product prefix, and this package prepends it — a call site never does prefix arithmetic and never omits it. Switching between the two when the bank answers is one line at composition, not a convention change audited across every call site.

Why the registration and not a parameter. An earlier design took the biller id and suffix as separate required inputs and called that the fix for cross-product collisions. It protects only the first row of that table. In the second, both products pass the same suffix and the discriminator becomes a free-text prefix that nothing validates — the original silent failure, with a call site that now looks guarded. Where both products' payments land in one receiving account that string is the only signal attributing a payment: not a formatting convention with a safety net behind it, but the safety net.

Reference length

BillerRegistration.MaxReferenceLength is derived from the format, not guessed. An EMVCo length field is two decimal digits, so Tag 30's whole value is capped at 99 characters, of which the AID takes 20 and a 15-digit Biller ID takes 19:

20 (AID) + 19 (biller id) + 4 (Ref1 header) + len(Ref1)  <=  99   =>   len(Ref1) <= 56

56 characters, or 53 once a 3-character product prefix is reserved. Supplying a Reference 2 lowers it further — that costs 4 characters plus its own length — and BillPayment checks the exact total, so an over-long reference is refused here rather than by a bank later.

Your bank's own limit may be lower. It is not knowable from here, so this package does not invent one: pass maxReferenceLength to tighten. It may only tighten, never widen.

Credit Transfer (Tag 29)

PromptPayQr.CreditTransfer(PromptPayProxy.MobileNumber("081-222-3333"), 590m);
PromptPayQr.CreditTransfer(PromptPayProxy.NationalId("1234567890123"));
PromptPayQr.CreditTransfer(PromptPayProxy.EWalletId("012345678901234"));

Tag 29 carries no reference fields. A payment made against one arrives with nothing identifying what it was for, so reconciliation falls back to amount and timestamp — which stops working the moment two payers owe the same amount in the same window. Use Bill Payment when payments have to be matched.

Mobile numbers: formatting characters (spaces, hyphens, parentheses, a leading +) are stripped and nothing else is inferred. What remains must be the 10-digit Thai national form or the 11-digit 66 form. A number in another country's national format is rejected rather than reinterpreted as Thai — guessing there does not fail, it succeeds, at whoever holds the resulting Thai number.

Amounts

Omit the amount for a reusable QR the payer types into (point of initiation 11); pass one for a one-time QR with the amount fixed (12). Amounts always render with two decimals.

Out of scope, permanently

Invoices, billing documents, reconciliation, running numbers, withholding tax, 50-Tawi certificates, and the decision of what a reference should contain. This package accepts a reference and a registration and constructs a correct payload; it does not decide what they mean.

Slip verification lives elsewhere — a service that only renders a QR must not depend on a verification client it has no credentials for.

Wire format

Pinned by golden vectors reproduced from an independent implementation and verified before this package existed, by recomputing every checksum with a bitwise CRC written from the algorithm rather than from a borrowed lookup table.

  • CRC-16/CCITT-FALSE, polynomial 0x1021, initial value 0xFFFF, over the payload including the 6304 that introduces the checksum tag, emitted as four uppercase hex digits.
  • Tag 29 AID A000000677010111; Tag 30 AID A000000677010112.
  • Root tags in the order 00, 01, 29/30, 53, 58, 54. EMVCo requires only that 00 comes first and the checksum last, but the checksum covers the whole string, so the order is fixed and the golden vectors break if it changes.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.30.4 90 10/4/2026
0.30.3 88 10/4/2026
0.30.2 84 10/3/2026
0.30.1 92 9/27/2026
0.30.0 89 9/27/2026
0.29.0 100 9/19/2026
0.28.1 103 9/19/2026
0.28.0 90 9/19/2026
0.27.0 100 9/19/2026
0.26.0 100 9/15/2026
0.25.1 107 9/11/2026
0.25.0 102 9/10/2026
0.24.0 98 9/10/2026
0.23.1 109 9/7/2026
0.23.0 115 9/7/2026
0.22.1 110 9/6/2026
0.22.0 110 9/5/2026
0.21.4 105 9/4/2026
0.21.3 107 8/30/2026
0.21.2 112 8/29/2026
Loading failed