Paynow Integration Skill
Integrating the Paynow Zimbabwe payment gateway without the bugs that cost merchants money.
paid() does not mean paid
Paynow’s documentation tells you to gate order fulfilment on paid(). Follow that
advice and you will lose orders.
A transaction is fully paid in three states — Paid, Awaiting Delivery and
Delivered. But the PHP SDK implements its helper as:
public function paid()
{
return $this->status() === 'paid';
}
So Awaiting Delivery returns false. The money arrives, your callback returns 200,
Paynow never retries, the dashboard shows the transaction as paid — and your customer
gets nothing. Silently, with no error and no log line.
The Node SDK is worse: it has no paid() method at all, yet the published
quickstart still shows if (status.paid()). That throws TypeError at runtime.
Paynow’s own worked example of a successful O’mari payment carries
status=Awaiting+Delivery. The evidence was always there.
Every claim on this site was checked against paynow/php-sdk and npm paynow v2.2.2
directly, not taken from the documentation.
Where to start
| Installing the skill | Put the skill in Claude Code or on claude.ai, and confirm it loaded. |
| Troubleshooting | Something is broken right now. Symptom-to-cause table. |
| Hashing & signatures | Hash mismatches, and the two fixtures Paynow publishes. |
| PHP & Laravel | SDK surface, the exceptions it throws, complete wiring. |
| Node.js & Express | The SDK’s two defects, raw client, callback route. |
| HTTP API reference | Every endpoint and field — for Python, Go, Java, C#. |
| Testing & go-live | Test numbers, card tokens, and the go-live checklist. |
The five rules that stop money going missing
- The
returnurlis cosmetic — never fulfil from it. Only the server-to-serverresulturlcallback is authoritative. paid()does not mean paid. Check the status word yourself against all three paid states.- Verify the hash over every value, in arrival order — never a documented field list. Paynow returns fields the docs omit, and they are inside the digest.
- Persist the
pollUrlbefore you redirect, and send a uniquemerchanttraceon Express Checkout. Otherwise a lost response is unrecoverable. - Make the callback idempotent and reconcile the amount. Paynow retries up to ten times and legitimately sends the same update more than once.
Install the skill
This reference also ships as a skill for Claude, so the guidance applies itself while you write the integration.
git clone https://github.com/67even/paynow-integration-skill.git
cp -r paynow-integration-skill/paynow-skills/paynow-integration ~/.claude/skills/
It includes two tools that need only Python and no dependencies: one that proves a hashing implementation against Paynow’s published fixtures in any language, and one that scans an existing codebase for the mistakes above.
cd paynow-integration-skill/paynow-skills/paynow-integration
python3 scripts/paynow_hash.py selftest # 4/4 expected
python3 scripts/audit_integration.py /path/to/project
Known upstream documentation defects
Places where the Paynow Developer Hub is wrong about its own SDKs, verified against the shipped packages on 20 September 2026.
| The hub says | The shipped SDK does | Severity |
|---|---|---|
paid() covers the paid states |
status() === 'paid' only |
Critical |
status.paid() (Node) |
No paid() exists |
Critical |
let status = paynow.pollTransaction(url) |
Returns a promise; must be awaited | High |
| Link-encoding recipe | Published examples are not reproducible from the documented steps | High |
InnBucks com.innbucks.customer:// |
npm paynow builds schinn.wbpycode:// |
Medium |
| Initiate response has four fields | Also returns paynowreference, which is inside the hash |
Medium |
| PHP constructor example | Missing a comma — does not parse | Medium |
| Shopify listed as a plugin | No self-serve setup; email sales@paynow.co.zw first |
Low |
If you re-check these against the hub later and find them disagreeing, the hub is the stale one — but please open an issue if the packages have changed.