Purpose of the Template
The X9.150 standard defines how merchants generate QR codes that customers scan to authorize push payments through FedNow, RTP, ACH, or other account-based rails. This template structures the payment request data your system must encode in those QR codes. It's not a policy document or a configuration file for your QR generator. It's the specification for what goes inside the code itself.
You'll use this when you're building or configuring a payment acceptance flow that supports merchant-presented QR codes. The standard creates a common structure so a customer's bank app can interpret your payment request without you building separate integrations for every financial institution.
The operational value isn't just speed. A properly structured X9.150 code carries invoice data, merchant identifiers, and transaction metadata that your reconciliation system can match against incoming funds. This means you can connect initiation with confirmation without manual lookup.
Prerequisites
Before you implement this template, confirm:
- Your payment processor or gateway supports X9.150 message structures and can route transactions across FedNow, RTP, or ACH based on availability and business rules.
- Your customer-facing systems can generate dynamic QR codes at checkout, on invoices, or in billing statements.
- You've defined how your system will handle payment notifications and match them to orders or invoices.
- Your fraud monitoring can validate payment requests against expected transaction patterns before encoding them in a code.
- You understand your liability model when customers scan codes in authenticated bank apps versus generic camera apps.
You don't need to know which rail will settle the transaction. The customer's financial institution determines routing based on what both sides support.
The Template
This is the data structure you encode in the QR code. Each field serves a specific function in the payment initiation process.
{
"version": "1.0",
"merchantIdentifier": {
"merchantID": "[Your unique merchant identifier]",
"merchantName": "[Legal business name]",
"merchantCategory": "[ISO 18245 MCC]"
},
"paymentRequest": {
"amount": "[Transaction amount in smallest currency unit]",
"currency": "USD",
"transactionID": "[Your internal transaction reference]",
"invoiceNumber": "[Customer-facing invoice or order number]",
"description": "[Payment purpose - 140 char max]"
},
"payee": {
"accountIdentifier": "[Your receiving account - format per rail]",
"routingNumber": "[ABA routing or equivalent]",
"accountType": "[checking|savings|business]"
},
"metadata": {
"billDate": "[ISO 8601 timestamp]",
"dueDate": "[ISO 8601 timestamp]",
"customerReference": "[Account number or customer ID]",
"paymentTerms": "[net30|immediate|other]"
},
"notification": {
"confirmationURL": "[Your webhook endpoint for payment status]",
"returnURL": "[Customer return destination after payment]"
},
"security": {
"codeExpiration": "[ISO 8601 timestamp - typically 15-30 min]",
"integrityHash": "[HMAC-SHA256 of payload fields]"
}
}
Customizing the Template
merchantIdentifier section: Replace bracketed values with your registered merchant credentials. The merchantID should match what you've registered with your processor. The merchantCategory uses the standard ISO 18245 merchant category code that defines your business type.
paymentRequest section: The amount must be in the smallest currency unit (cents for USD). Your transactionID is your internal reference. The invoiceNumber is what the customer sees on their statement. Keep the description concise but specific enough for the customer to recognize the charge.
payee section: This is your receiving account information. The accountIdentifier format depends on which rails your processor supports. For ACH, you'll use account number. For RTP, you might use an alias. Your processor should provide the correct format. Don't hardcode credentials here; pull them from your secure configuration store at generation time.
metadata section: These fields drive reconciliation. The customerReference should match what's in your accounts receivable system. Include it even if the customer doesn't see it. When the payment notification arrives, you'll match it against this reference to close the invoice automatically.
notification section: The confirmationURL is where the customer's bank sends payment status. It must be a publicly accessible HTTPS endpoint that can process asynchronous POST requests. The returnURL is where the customer lands after completing payment in their bank app. Make it a confirmation page, not your homepage.
security section: The codeExpiration prevents replay attacks. Set it based on your use case. A point-of-sale code might expire in 15 minutes. A utility bill code might last 24 hours. The integrityHash protects the payload from tampering. Generate it by concatenating critical fields (amount, transactionID, merchantID, timestamp) and hashing with a secret key only your system and your processor share.
Validation Steps
Before you generate codes in production, test these scenarios:
Field completeness: Generate a code with all fields populated. Scan it in a test bank app (your processor should provide one). Verify every field appears correctly in the payment preview screen.
Amount precision: Generate codes for $0.01, $1.00, $999.99, and $1,000.00. Confirm the amounts display correctly without rounding errors or decimal shifts.
Expiration handling: Generate a code with a 5-minute expiration. Wait 6 minutes. Attempt to scan it. The bank app should reject it with an expiration error, not process a stale payment.
Notification receipt: Complete a test payment. Your confirmationURL should receive a POST within 30 seconds containing the transaction status, amount, timestamp, and your original transactionID. Log the full payload and verify you can parse it.
Reconciliation match: After receiving a payment notification, confirm your system can automatically match the incoming payment to the correct invoice using the customerReference and transactionID. If you can't match it programmatically, you haven't structured your metadata correctly.
Hash verification: Modify one character in the amount field of a generated code. Scan it. Your processor should reject it due to hash mismatch. If it doesn't, your integrity protection isn't working.
Test across multiple customer banks if your processor supports it. The X9.150 structure should work identically regardless of which institution processes the payment. If you see different behavior, that's a processor integration issue, not a template problem.
When you move to production, monitor your confirmationURL for errors. A spike in timeout errors means your endpoint can't handle the notification volume. A spike in parsing errors means the notification format doesn't match what you're expecting. Both require immediate attention because they break your reconciliation flow.



