Templates & tools
Every template already follows the payment-flow rules:
- AUTH before charging the customer;
- PAY sent once;
- STATUS polling at 120/180/600 s, with
BeingPaidandPendingtreated as pending; - each receipt and SMS delivered separately;
- the forex flag copied from AUTH;
- a filtered
ListBillerscall after the config webhook; X-Signatureverified over the raw body.
Adapt them rather than pasting them blind. The comments explain why each guard is there.
Contents
Supported languages
| Stack | Minimum | What ships |
|---|---|---|
| PHP / Laravel | PHP 8.1+, Laravel 10+ (tested on 13) | HTTP client, payment service, purchase + wallet + both webhook controllers, form request, three models, three queued jobs, migration, routes, 11 feature tests |
| Python | Python 3.9+ | requests client covering the Vendor and Biller APIs, purchase flow with injectable hooks, Flask webhook receivers |
| Node.js / TypeScript | Node 18+ | Dependency-free fetch client, purchase flow, Express webhook receivers (strict tsc clean) |
| C# / .NET | .NET 8 | Typed HttpClient, full models, purchase flow, ASP.NET Core minimal-API webhooks |
Any other language can use the API reference and the vendor workflows.
PHP / Laravel
Located in templates/php-laravel/. Its README.md says where each file goes.
php artisan migrate
php artisan billpay:seed-catalogue # the only unfiltered ListBillers call
php artisan queue:work # polling jobs are delayed; they can't run on "sync"
php artisan test --filter=BillPayTest # 11 tests, fake HTTP, no credentials needed
Implement BillPayHooks (charge, refund, receipt, SMS, alerts) and bind it in a service
provider. Nothing is sold until you do: a failed charge never reaches PAY.
Python
Located in templates/python/:
billpay_client.pyhasBillPayClient,purchase()andpoll_until_final().webhooks_flask.pyhas both webhook receivers.
In production, run polling in a worker (Celery, RQ), not in the request.
Node.js / TypeScript
Located in templates/node/:
billpayClient.tsis the client.purchase.tsis the purchase flow.webhooks.tsis the Express receivers, with raw-body HMAC and an Express 4-safe async error path.
C# / .NET
Located in templates/dotnet/:
BillPayClient.csis a typedHttpClientthat omits null fields.BillPayModels.cshas the models.PurchaseFlow.csis the purchase flow.Webhooks.cshas the minimal-API receivers, whose legacy-hash check reproduces Paynow’s worked example.
Tools
Both need only Python 3.8+ and the standard library.
billpay_cli.py: smoke-test from the command line
python3 scripts/billpay_cli.py wallets
python3 scripts/billpay_cli.py billers --codes ZETDC,EVD
python3 scripts/billpay_cli.py member <BILLER> <MEMBER>
python3 scripts/billpay_cli.py auth TEST PP12345 --product AI --ref <uuid>
python3 scripts/billpay_cli.py status <uuid>
It prints the HTTP status and the next step for the returned Status, for example
pending: poll every 180 s (first at 120 s).
verify_webhook.py: prove a webhook signature
python3 scripts/verify_webhook.py --self-test
python3 scripts/verify_webhook.py body.json --secret KEY --signature "<X-Signature>"
python3 scripts/verify_webhook.py body.json --secret KEY # legacy Hash field
On a mismatch it prints the concatenated plaintext, so a wrong field order or price format is visible immediately.
This page is maintained by hand. The starter code lives in
paynow-paybill-skills/paynow-billpay/templates/.
Spotted something wrong? Open an issue.