A Codex-first template for adding invoice-specific PayNow QR payments to Xero invoices. It derives a dynamic QR from merchant fields decoded from the official provider-issued QR, including support for virtual payment addresses (VPA).
This software does not register a merchant for PayNow, confirm receipt of money, or reconcile Xero. Complete a real low-value banking-app test before customer rollout.
- An activated Singapore PayNow receiving service and the official provider-issued QR as a PNG file.
- A registered recipient name that can be confirmed through a real banking-app scan.
- Node.js 20.20 or newer, npm and Git on the machine running the setup.
- Access to the Xero organisation's payment-service and branding-theme settings.
- An HTTPS payment-page URL. A custom domain is optional, but using one requires DNS access.
- Explicit approval before the agent changes hosting, DNS or Xero.
Check the local toolchain with:
node --version
npm --version
git --versionYou do not need both Netlify and a VPS.
Required:
- A Netlify account and permission to create or manage the target site.
- Either Git-based deployment from your copy of this repository, or Netlify CLI access for an agent-led/manual deployment.
- Netlify CLI authentication when using the CLI path.
- DNS access if attaching a custom domain.
Recommended CLI checks:
npx netlify --version
npx netlify statusFor a new Git-connected site, use npx netlify init. For a manual deployment, first create a draft with npx netlify deploy --dir=dist, verify its preview URL, then promote with npx netlify deploy --prod --dir=dist.
The VITE_ merchant fields are compiled into the browser and are not secrets. Configure them for the intended Netlify deploy context; never place tokens or private credentials in VITE_ variables.
Required:
- A Linux VPS and authorised SSH access.
- Docker Engine with the Docker Compose plugin.
- A reverse proxy such as Caddy or Nginx terminating HTTPS.
- DNS access for the payment-page hostname.
- A firewall allowing public HTTPS while the application container remains bound to host loopback.
Recommended checks:
ssh <host> 'docker --version && docker compose version'
The included Compose file binds the application to 127.0.0.1:18080. The reverse proxy should be the only public path to it. Netlify CLI is not needed for this route.
Give Codex this repository URL and say:
Set this up for my Xero organisation. Follow AGENTS.md. Start by asking me for my provider-issued PayNow QR and intended payment-page domain. Do not change Xero until I approve the exact payment-service change.
Codex should first audit the prerequisites and ask which hosting path to use. It should then decode the QR, prepare .env.local, test the generated payload, deploy the page and guide the final Xero setup.
PayNow receiving addresses are not interchangeable. A provider QR may use a mobile number, UEN or VPA. For example, a VPA can resemble a UEN while using a different PayNow proxy type. Guessing the proxy type can produce a valid-looking QR that banking apps report as unregistered.
The decoder extracts the provider's authoritative fields instead:
npm install
npm run decode-qr -- C:\path\to\provider-paynow.pngKeep the original QR and generated .env.local private.
Copy-Item .env.example .env.local
npm test
npm run buildRequired settings:
| Variable | Source |
|---|---|
VITE_PAYNOW_PROXY_TYPE |
PayNow merchant account field 01 |
VITE_PAYNOW_PROXY |
PayNow merchant account field 02 |
VITE_PAYNOW_RECIPIENT_DISPLAY |
Registered recipient name confirmed by a bank scan |
VITE_PAYNOW_QR_MERCHANT_NAME |
Top-level merchant-name field 59 |
VITE_PAYNOW_MCC |
Merchant category field 52 |
VITE_PAYNOW_COUNTRY |
Country field 58 |
VITE_PAYNOW_CITY |
City field 60 |
VITE_PAYNOW_POSTAL_CODE |
Postal field 61, when present |
The public URL accepts only transaction values:
https://pay.example.com/?invoiceNo=INV-1234¤cy=SGD&amount=100.00
Merchant identity is compiled from trusted configuration and cannot be replaced through URL parameters.
After the page is deployed and a bank-app scan succeeds, configure Xero with:
https://pay.example.com/?invoiceNo=[INVOICENUMBER]¤cy=[CURRENCY]&amount=[AMOUNTDUE]&shortCode=[SHORTCODE]
Attach the payment service only to the intended branding themes. This page does not create Xero payments or mark invoices paid.
The project builds to dist/ and can be hosted on Netlify or another static host. A hardened Nginx container is also included:
docker compose up --buildThe container binds to 127.0.0.1:18080; place it behind a TLS reverse proxy for public use.
npm testandnpm run buildpass.- The official QR fields were decoded instead of guessed.
- The live page rejects missing or non-SGD invoice parameters.
- A banking app shows the expected recipient.
- The amount and invoice reference are populated correctly.
- The merchant confirms receipt from bank evidence, not from the payer's screenshot.
See NOTICE.md. This repository is not affiliated with Xero, PayNow, ABS, MAS or any bank/payment provider.