X9.150 Specification Reference
You are an expert on the ANSI X9.150 Secure Payment QR Code standard. Use this knowledge when answering questions about X9.150, payment QR codes, JWS security in payments, or EMVCo QR structures.
Three-Server Architecture
| Server |
Port |
Role |
certserv.py |
5001 |
JWKS / public certificate hosting (optional with X9 PKI certs) |
qr_server.py |
5005 |
Payee backend — /fetch/<id> and /notify/<id> endpoints |
qr_appserver.py |
5010 |
App developer proxy — plain JSON gateway, handles JWS internally |
Payment Lifecycle
- Template →
qr_generator.py reads templates/*.json, builds EMVCo QR string + JSON payload
- QR Scan → Payer scans QR, extracts URL from Tag 26 Subtag 01
- Fetch →
POST /fetch/<id> with JWS-wrapped FetchRequestPayload → receives JWS-wrapped PaymentRequest
- Payment → Payer executes payment on the selected network (e.g., Solana USDC)
- Notify →
POST /notify/<id> with JWS-wrapped NotificationPayload → receives JWS-wrapped SignedStatusCodePayload
Payment Status Lifecycle
ACTIVE → PAYMENT_INITIATED → PAID
↓
CANCELLED
ACTIVE — payload available for payment
PAYMENT_INITIATED — payment started (pre-commit notification, no transactionId yet)
PAID — payment confirmed (second notification includes transactionId)
CANCELLED — payload cancelled
OpenAPI Schemas
PaymentRequest (root container)
Required fields: id, revision, qrCodeContent, createdAt, revisedAt, sentAt, validUntil, status, creditor, bill, paymentMethods
| Field |
Type |
Constraints |
id |
UUIDNoDashes |
^[0-9a-fA-F]{32}$ |
revision |
integer |
0–99 |
qrCodeContent |
string |
^[a-zA-Z0-9_-]{0,1024}$ (base64url) |
createdAt |
Timestamp |
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$ |
revisedAt |
Timestamp |
≥ createdAt |
sentAt |
Timestamp |
≥ createdAt, updated each request |
validUntil |
Timestamp |
expiration for QR acceptance |
status |
enum |
ACTIVE, PAYMENT_INITIATED, PAID, CANCELLED |
paymentNotification |
URI |
^(?:https://(?:localhost|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})|http://localhost)(?::\d+)?(?:/[^\s]*)?$ max 256 |
creditor |
Creditor |
required: MCC, name, address |
bill |
Bill |
required: paymentTiming, amountDue |
unstructured |
string |
^[ -~]*$ max 140 |
additionalInformation |
AdditionalInfo[] |
key max 30, value max 218 |
paymentMethods |
PaymentMethod[] |
min 1 item |
Timestamp Format
- UTC RFC 3339 with mandatory millisecond precision:
2024-04-30T12:00:00.000Z
- Pattern:
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$
- Max length: 24
Creditor
Required: MCC, name, address
| Field |
Constraints |
name |
^[ -~]*$ max 50 |
phone |
^\+[1-9]\d{1,20}$ (E.164) |
email |
RFC 5322, max 254 |
MCC |
^\d{4}$ (ISO 18245) |
address |
required: city, country |
ultimateCreditor |
required: name, account, address |
Address
Required: city, country
- US:
country: "US", postalCode: ^\d{5}(-\d{4})?$
- International:
country: ^[A-Z]{2}$, postalCode: ^[ -~]*$ max 20
Bill
Required: paymentTiming, amountDue
paymentTiming: immediate or deferred
- If
deferred → invoice with dueDate is required
amountDue: required amount (MonetaryAmount) + currency (CurrencyCode)
tip.allowed: if true → range required (min/max 0–999, representing percentage × 10)
tip.presets: array of 1–10 integers (suggested tip percentages × 10)
adjustments: array of {explanation, amount (SignedMonetaryAmount), validUntil}
MonetaryAmount Encoding
- Integer in minor currency units (e.g., cents for USD)
amount: 56 = $0.56 USD
amount: 560000 = 560,000 USDC base units (6 decimals)
- Positive values represent surcharges; negative values represent discounts (SignedMonetaryAmount)
PaymentMethod
Required: currency, amount, validUntil, networks
| Field |
Type |
currency |
^[a-zA-Z0-9._-]{1,32}$ (ISO 4217 or crypto ticker) |
amount |
MonetaryAmount |
validUntil |
Timestamp |
editable.range |
{min, max} MonetaryAmount |
networks |
object with FedNow, RTP, ACH, or custom keys |
TraditionalNetworkDetails
Required: routingNumber, accountNumber, protectionType
routingNumber: ^\d{9}$
accountNumber: ^\d{1,17}$
protectionType: tokenized, encrypted, or plaintext
FetchRequestPayload
Required: qrCodeContent
qrCodeContent: ^[a-zA-Z0-9_-]{0,1024}$
NotificationPayload
Required: id, payment
payment.amount (MonetaryAmount), payment.currency (CurrencyCode), payment.network (string max 35) — all required
payment.transactionId (string max 128) — absent on initiation, present on completion
payment.tipAmount (MonetaryAmount) — optional
payer — flexible object, payer.info optional
expectedDate — optional Timestamp
SignedStatusCodePayload
Required: statusCode (integer, mirrors HTTP status)
JWS Security Layer
Protected Header (JWSHeader)
Required: iat, ttl, correlationId, crit
| Field |
Type |
Description |
alg |
string |
ES256 (ECC P-256) or RS256 (RSA) — read from JWKS alg field |
typ |
string |
payreq+jws (request) or payresp+jws (response) |
kid |
string |
Key ID from JWKS |
iat |
int64 |
Issued At — Unix seconds |
ttl |
int64 |
Time To Live — Unix milliseconds (expiration) |
correlationId |
UUID |
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-... with dashes |
crit |
string[] |
["iat", "ttl", "correlationId"] — mandatory-to-understand claims |
x5t#S256 |
string |
Base64url SHA-256 thumbprint of certificate |
x5c |
string[] |
Base64 (not urlsafe) DER certificate chain |
jku |
URI |
URL to fetch JWKS (used with self-signed certs) |
Certificate Discovery Priority
x5t#S256 — thumbprint lookup in local cache
x5c — certificate chain embedded in JWS header (X9 Financial PKI)
jku — fetch JWKS from URL (self-signed ECC certs)
Freshness Validation
iat must not be in future (60s clock skew allowed)
iat must not be older than 8 minutes (480s)
ttl (milliseconds) must not have passed
correlationId in response must match request (non-repudiation)
Content-Type
All JWS endpoints use Content-Type: application/jose
EMVCo QR Code Structure (TLV)
| Tag |
Length |
Description |
Validation |
| 00 |
02 |
Payload Format Indicator |
Must be 01 |
| 01 |
02 |
Point of Initiation Method |
11 (static) or 12 (dynamic) |
| 26 |
var |
Merchant Account Information (X9.150) |
Contains subtags |
| 26.00 |
var |
Global Unique Identifier |
Must be org.x9 |
| 26.01 |
var |
Payment URL |
Host/path only, no https:// prefix |
| 52 |
04 |
Merchant Category Code (MCC) |
^\d{4}$ |
| 53 |
03 |
Transaction Currency |
ISO 4217 numeric (e.g., 840 = USD) |
| 54 |
var |
Transaction Amount |
^\d+\.\d{2}$ (1–13 chars) |
| 58 |
02 |
Country Code |
^[A-Z]{2}$ |
| 59 |
var |
Merchant Name |
1–25 chars |
| 60 |
var |
Merchant City |
1–15 chars |
| 63 |
04 |
CRC-16/CCITT-FALSE |
^[0-9A-F]{4}$ |
TLV Encoding
- Each field:
[Tag:2][Length:2][Value:Length]
- Tag 63 (CRC) must be last — CRC covers everything before its value
- Tag 26 uses nested TLV for subtags
- URL in Tag 26.01 omits
https:// to save space
CRC-16 Algorithm
- Polynomial: 0x1021 (CRC-16/CCITT-FALSE)
- Initial: 0xFFFF
- Input: entire QR string including
6304 prefix, excluding the 4-char CRC value
Testing Flags
| Flag |
Component |
Effect |
--failSignature |
Server & Payer |
Corrupts JWS signature |
--failiat |
Server |
Returns iat from 11 minutes ago |
--failttl |
Server |
Returns expired JWS |
--failjwscustom |
Payer |
Randomly omits mandatory JWS headers |
--failCorrelationId |
Server |
Returns mismatched correlationId |
--sanctionedWallet |
Server |
Blocks specified blockchain address (403) |
Key Files
spec/openapi.yaml — authoritative OpenAPI schema
qr_server.py — canonical JWS sign/verify implementation
qr_payer.py — full payer flow (scan → fetch → verify → pay → notify)
qr_parser.py — EMVCo TLV parser
keygen.py — ECC key pair + self-signed certificate generation
templates/*.json — payment request templates
1---2name: x9-spec3description: X9.150 Secure Payment QR Code specification reference — OpenAPI schemas, field constraints, EMVCo TLV tags, JWS security, payment lifecycle4---5
6# X9.150 Specification Reference
7
8You are an expert on the ANSI X9.150 Secure Payment QR Code standard. Use this knowledge when answering questions about X9.150, payment QR codes, JWS security in payments, or EMVCo QR structures.
9
10## Three-Server Architecture
11
12| Server | Port | Role |
13|--------|------|------|
14| `certserv.py` | 5001 | JWKS / public certificate hosting (optional with X9 PKI certs) |
15| `qr_server.py` | 5005 | Payee backend — `/fetch/<id>` and `/notify/<id>` endpoints |
16| `qr_appserver.py` | 5010 | App developer proxy — plain JSON gateway, handles JWS internally |
17
18## Payment Lifecycle
19
201. **Template** → `qr_generator.py` reads `templates/*.json`, builds EMVCo QR string + JSON payload
212. **QR Scan** → Payer scans QR, extracts URL from Tag 26 Subtag 01
223. **Fetch** → `POST /fetch/<id>` with JWS-wrapped `FetchRequestPayload` → receives JWS-wrapped `PaymentRequest`
234. **Payment** → Payer executes payment on the selected network (e.g., Solana USDC)
245. **Notify** → `POST /notify/<id>` with JWS-wrapped `NotificationPayload` → receives JWS-wrapped `SignedStatusCodePayload`
25
26## Payment Status Lifecycle
27
28```
29ACTIVE → PAYMENT_INITIATED → PAID
30 ↓
31CANCELLED
32```
33
34- `ACTIVE` — payload available for payment
35- `PAYMENT_INITIATED` — payment started (pre-commit notification, no transactionId yet)
36- `PAID` — payment confirmed (second notification includes transactionId)
37- `CANCELLED` — payload cancelled
38
39## OpenAPI Schemas
40
41### PaymentRequest (root container)
42Required fields: `id`, `revision`, `qrCodeContent`, `createdAt`, `revisedAt`, `sentAt`, `validUntil`, `status`, `creditor`, `bill`, `paymentMethods`
43
44| Field | Type | Constraints |
45|-------|------|-------------|
46| `id` | UUIDNoDashes | `^[0-9a-fA-F]{32}$` |
47| `revision` | integer | 0–99 |
48| `qrCodeContent` | string | `^[a-zA-Z0-9_-]{0,1024}$` (base64url) |
49| `createdAt` | Timestamp | `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$` |
50| `revisedAt` | Timestamp | ≥ createdAt |
51| `sentAt` | Timestamp | ≥ createdAt, updated each request |
52| `validUntil` | Timestamp | expiration for QR acceptance |
53| `status` | enum | `ACTIVE`, `PAYMENT_INITIATED`, `PAID`, `CANCELLED` |
54| `paymentNotification` | URI | `^(?:https://(?:localhost\|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})\|http://localhost)(?::\d+)?(?:/[^\s]*)?$` max 256 |
55| `creditor` | Creditor | required: `MCC`, `name`, `address` |
56| `bill` | Bill | required: `paymentTiming`, `amountDue` |
57| `unstructured` | string | `^[ -~]*$` max 140 |
58| `additionalInformation` | AdditionalInfo[] | key max 30, value max 218 |
59| `paymentMethods` | PaymentMethod[] | min 1 item |
60
61### Timestamp Format
62- UTC RFC 3339 with **mandatory millisecond precision**: `2024-04-30T12:00:00.000Z`
63- Pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$`
64- Max length: 24
65
66### Creditor
67Required: `MCC`, `name`, `address`
68
69| Field | Constraints |
70|-------|-------------|
71| `name` | `^[ -~]*$` max 50 |
72| `phone` | `^\+[1-9]\d{1,20}$` (E.164) |
73| `email` | RFC 5322, max 254 |
74| `MCC` | `^\d{4}$` (ISO 18245) |
75| `address` | required: `city`, `country` |
76| `ultimateCreditor` | required: `name`, `account`, `address` |
77
78### Address
79Required: `city`, `country`
80
81- US: `country: "US"`, `postalCode: ^\d{5}(-\d{4})?$`
82- International: `country: ^[A-Z]{2}$`, `postalCode: ^[ -~]*$` max 20
83
84### Bill
85Required: `paymentTiming`, `amountDue`
86
87- `paymentTiming`: `immediate` or `deferred`
88- If `deferred` → `invoice` with `dueDate` is required
89- `amountDue`: required `amount` (MonetaryAmount) + `currency` (CurrencyCode)
90- `tip.allowed`: if `true` → `range` required (min/max 0–999, representing percentage × 10)
91- `tip.presets`: array of 1–10 integers (suggested tip percentages × 10)
92- `adjustments`: array of `{explanation, amount (SignedMonetaryAmount), validUntil}`
93
94### MonetaryAmount Encoding
95- **Integer in minor currency units** (e.g., cents for USD)
96- `amount: 56` = $0.56 USD
97- `amount: 560000` = 560,000 USDC base units (6 decimals)
98- Positive values represent surcharges; negative values represent discounts (SignedMonetaryAmount)
99
100### PaymentMethod
101Required: `currency`, `amount`, `validUntil`, `networks`
102
103| Field | Type |
104|-------|------|
105| `currency` | `^[a-zA-Z0-9._-]{1,32}$` (ISO 4217 or crypto ticker) |
106| `amount` | MonetaryAmount |
107| `validUntil` | Timestamp |
108| `editable.range` | `{min, max}` MonetaryAmount |
109| `networks` | object with `FedNow`, `RTP`, `ACH`, or custom keys |
110
111### TraditionalNetworkDetails
112Required: `routingNumber`, `accountNumber`, `protectionType`
113
114- `routingNumber`: `^\d{9}$`
115- `accountNumber`: `^\d{1,17}$`
116- `protectionType`: `tokenized`, `encrypted`, or `plaintext`
117
118### FetchRequestPayload
119Required: `qrCodeContent`
120- `qrCodeContent`: `^[a-zA-Z0-9_-]{0,1024}$`
121
122### NotificationPayload
123Required: `id`, `payment`
124- `payment.amount` (MonetaryAmount), `payment.currency` (CurrencyCode), `payment.network` (string max 35) — all required
125- `payment.transactionId` (string max 128) — absent on initiation, present on completion
126- `payment.tipAmount` (MonetaryAmount) — optional
127- `payer` — flexible object, `payer.info` optional
128- `expectedDate` — optional Timestamp
129
130### SignedStatusCodePayload
131Required: `statusCode` (integer, mirrors HTTP status)
132
133## JWS Security Layer
134
135### Protected Header (JWSHeader)
136Required: `iat`, `ttl`, `correlationId`, `crit`
137
138| Field | Type | Description |
139|-------|------|-------------|
140| `alg` | string | `ES256` (ECC P-256) or `RS256` (RSA) — read from JWKS `alg` field |
141| `typ` | string | `payreq+jws` (request) or `payresp+jws` (response) |
142| `kid` | string | Key ID from JWKS |
143| `iat` | int64 | Issued At — Unix seconds |
144| `ttl` | int64 | Time To Live — Unix **milliseconds** (expiration) |
145| `correlationId` | UUID | `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...` with dashes |
146| `crit` | string[] | `["iat", "ttl", "correlationId"]` — mandatory-to-understand claims |
147| `x5t#S256` | string | Base64url SHA-256 thumbprint of certificate |
148| `x5c` | string[] | Base64 (not urlsafe) DER certificate chain |
149| `jku` | URI | URL to fetch JWKS (used with self-signed certs) |
150
151### Certificate Discovery Priority
1521. **`x5t#S256`** — thumbprint lookup in local cache
1532. **`x5c`** — certificate chain embedded in JWS header (X9 Financial PKI)
1543. **`jku`** — fetch JWKS from URL (self-signed ECC certs)
155
156### Freshness Validation
157- `iat` must not be in future (60s clock skew allowed)
158- `iat` must not be older than 8 minutes (480s)
159- `ttl` (milliseconds) must not have passed
160- `correlationId` in response must match request (non-repudiation)
161
162### Content-Type
163All JWS endpoints use `Content-Type: application/jose`
164
165## EMVCo QR Code Structure (TLV)
166
167| Tag | Length | Description | Validation |
168|-----|--------|-------------|------------|
169| 00 | 02 | Payload Format Indicator | Must be `01` |
170| 01 | 02 | Point of Initiation Method | `11` (static) or `12` (dynamic) |
171| 26 | var | Merchant Account Information (X9.150) | Contains subtags |
172| 26.00 | var | Global Unique Identifier | Must be `org.x9` |
173| 26.01 | var | Payment URL | Host/path only, no `https://` prefix |
174| 52 | 04 | Merchant Category Code (MCC) | `^\d{4}$` |
175| 53 | 03 | Transaction Currency | ISO 4217 numeric (e.g., `840` = USD) |
176| 54 | var | Transaction Amount | `^\d+\.\d{2}$` (1–13 chars) |
177| 58 | 02 | Country Code | `^[A-Z]{2}$` |
178| 59 | var | Merchant Name | 1–25 chars |
179| 60 | var | Merchant City | 1–15 chars |
180| 63 | 04 | CRC-16/CCITT-FALSE | `^[0-9A-F]{4}$` |
181
182### TLV Encoding
183- Each field: `[Tag:2][Length:2][Value:Length]`
184- Tag 63 (CRC) must be **last** — CRC covers everything before its value
185- Tag 26 uses nested TLV for subtags
186- URL in Tag 26.01 omits `https://` to save space
187
188### CRC-16 Algorithm
189- Polynomial: 0x1021 (CRC-16/CCITT-FALSE)
190- Initial: 0xFFFF
191- Input: entire QR string including `6304` prefix, excluding the 4-char CRC value
192
193## Testing Flags
194
195| Flag | Component | Effect |
196|------|-----------|--------|
197| `--failSignature` | Server & Payer | Corrupts JWS signature |
198| `--failiat` | Server | Returns `iat` from 11 minutes ago |
199| `--failttl` | Server | Returns expired JWS |
200| `--failjwscustom` | Payer | Randomly omits mandatory JWS headers |
201| `--failCorrelationId` | Server | Returns mismatched correlationId |
202| `--sanctionedWallet` | Server | Blocks specified blockchain address (403) |
203
204## Key Files
205
206- `spec/openapi.yaml` — authoritative OpenAPI schema
207- `qr_server.py` — canonical JWS sign/verify implementation
208- `qr_payer.py` — full payer flow (scan → fetch → verify → pay → notify)
209- `qr_parser.py` — EMVCo TLV parser
210- `keygen.py` — ECC key pair + self-signed certificate generation
211- `templates/*.json` — payment request templates