Paynow-GO
An idiomatic, dependency free Go SDK for the Paynow Zimbabwe payment gateway. Web redirects, mobile express checkout on EcoCash, OneMoney and InnBucks, status polling and hash verified webhooks, behind an API that feels like the standard library.

Project Overview
Paynow is the payment gateway most Zimbabwean businesses actually use, and for years it shipped official SDKs for Node.js and Python while Go developers were left copying the same hand rolled HTTP client between projects. I got tired of writing that code for the third time, so I wrote it once, properly, and gave it away under MIT. The repo description says it best: built for the Zimbabwe Go community so we do not have to keep reinventing the wheel.
It has zero dependencies. The go.mod file is three lines: module path, Go version, nothing else. Standard library only. For an SDK that is not showing off, it is a feature. Nobody wants to pull a transitive dependency tree into their payment path, and a library that only needs net/http will still compile in five years.
The API is the part I cared about most. The client takes your integration id and key plus functional options, so `WithResultURL`, `WithReturnURL` and `WithHTTPClient` compose without a config struct that grows forever. Payments behave like a shopping cart: create one against a reference and an email, call Add as many times as you like with a title, a unit amount and an optional quantity, and the total computes itself. Every network call takes a context as its first argument, because a payment call that cannot be cancelled or timed out is a liability.
Two payment paths are supported. Web payments hand you a redirect URL to send the customer to Paynow and a poll URL to check on the transaction afterwards. Mobile express checkout charges EcoCash, OneMoney or InnBucks directly and returns the USSD instructions to show the customer. InnBucks gets its own extras, since it answers with an authorization code, a deep link and a QR code rather than a prompt, so those come back on their own struct instead of being flattened into loose strings.
The most interesting bug I never shipped was about map ordering. Paynow authenticates requests and responses by concatenating field values in a specific order, appending your integration key, and hashing the lot with SHA-512. The obvious thing to reach for in Go is url.Values, which is a map, and Go's standard encoder sorts map keys when it renders a form body. Sorted keys produce a correctly formatted request with a hash that will never validate, and you get to spend an afternoon wondering why the gateway hates you. So the SDK has its own small ordered values type that keeps insertion order for both request building and response parsing, and the hashing lives in an internal package where callers cannot misuse it.
Verification is not optional either. Every response that should carry a hash is checked before you see it, and a mismatch fails with a distinct error rather than a boolean you might ignore. That matters most on the result URL webhook: you hand `ProcessStatusUpdate` the raw request body, it verifies the hash, and only then do you fulfil the order. Signature verification you have to remember to call is signature verification nobody calls.
Errors are split by who caused them. Network, parsing and hash failures come back as ordinary errors. Things Paynow itself complains about, like a bad integration id, come back as an APIError you can pull the gateway's own message out of with errors.As, and the parsed response is still attached so you can inspect it. On top of that there are sentinel errors for the mistakes callers actually make: no payment, empty cart, a total that is not positive, a mobile payment with no valid auth email, a missing hash, a hash that does not match. All matchable with errors.Is, all documented in a table in the README.
Because the HTTP client is an interface with a single Do method, and *http.Client already satisfies it, testing needs no mocking framework and no live credentials. There are test files covering the payment builder, sending, polling, status parsing, the hashing package and the shared helpers. CI runs the suite across Go 1.21, 1.22 and 1.23 with the race detector on, pushes coverage to Codecov, and runs go vet, staticcheck and golangci-lint as separate jobs, with the build gated behind all three passing. It is more rigour than a small SDK strictly needs, but this is the kind of library other people drop into their checkout flow, and I would rather it be boring and trustworthy than clever.
There is a full runnable flow in example/main.go, so integrating is a copy, paste and swap in your own credentials. Three stars and a fork is not exactly a landslide, but every one of them is a Zimbabwean developer who did not have to write this code themselves, which was the entire point.
Key Features
- Web payments returning a redirect URL for the customer and a poll URL for you
- Mobile express checkout charging EcoCash, OneMoney or InnBucks directly
- InnBucks responses carrying the authorization code, deep link and QR code
- Payments that behave like a cart, computing their own total as you add items
- Functional options for the result URL, return URL and a custom HTTP client
- Context on every network call, so payments can be cancelled and timed out
- SHA-512 request signing with automatic verification of every response hash
- Insertion ordered request building, because Paynow hashes fields in order
- Result URL webhook handling that verifies the hash before you trust the update
- Transaction status helpers instead of comparing raw gateway strings
- Sentinel errors for caller mistakes and a distinct APIError for gateway complaints
- A single method HTTP interface, so tests need no mocking framework or live keys
- Zero third party dependencies: the go.mod file is three lines long
- A complete runnable flow in example/main.go
Challenges & Solutions
Go's form encoder sorts map keys, which silently breaks a hash that depends on field order
Porting from the official Node.js and Python SDKs without importing their dynamic habits into Go
Deciding what belongs in an internal package so callers cannot misuse the hashing
Modelling InnBucks, which answers with a code, a deep link and a QR code instead of a USSD prompt
Separating caller mistakes from gateway errors so both are matchable rather than stringly typed
Keeping the dependency list empty while still being pleasant to test against
Key Learnings
Project Info
Timeline
6 months, on and off
Team
Solo, open source
Status
Tech Stack
Language
Payments
Security
Testing
CI
Distribution