← All posts
·6 min read·Kennedy Owusu

Four things a tax authority told us that its documentation didn't

We integrated Koolbase with Ghana's e-VAT system. The published spec was accurate. It was also incomplete in four ways that only a real request could reveal.

A growing number of countries now require every sales invoice to be reported to the tax authority in real time. Ghana is one of them. If you build a point-of-sale app here, you can't just record a sale in your database and print a receipt — the receipt has to carry a signature and a QR code that the Ghana Revenue Authority issued for that specific transaction.

So I built that into Koolbase. GRA publishes an integration specification, and it's a good one: endpoints, field tables, sample payloads, even working test credentials. I read it carefully before writing a line of code.

Then I sent a real request, and learned four things the document doesn't say.

1. The error format was different

The spec documents what happens when you submit an invoice number that's already been used. There's a sample response, with a specific shape and a specific field carrying a specific message.

The live system answers with a different shape entirely — different field names, a structured error code the spec never mentions.

This matters more than it sounds. That particular error is not really an error: it's the authority telling you "I already have this one." If your code doesn't recognise it, you interpret a success as a failure. And in a system where every invoice number is spent permanently, misreading "already done" as "rejected" is how you end up with a hole in a legally-mandated sequence.

We'd matched the documented format exactly. Against the real system, our matcher would have silently never fired.

2. Refunds resolve against the original invoice

The spec shows a refund payload. It has an invoice number field and a reference field. Reading it, I concluded the invoice number was the refund's own new number, and the reference was a pointer to the original sale.

Backwards. The authority resolves a refund against the invoice number — that field must name the original, already-certified invoice. The reference is the refund's own identity.

I only found out because a refund came back rejected with an error naming the number we'd generated: not found. Of course it wasn't found. We'd invented it.

3. The authority enforces business rules you'd assume were yours

We fully refunded an invoice, then tried a partial refund on the same one — sloppy test sequencing on our part. The response wasn't a validation error about the payload. It was the authority saying: that invoice has already been refunded.

Which was quietly reassuring. It means the authority is doing balance arithmetic on its side, and a client bug can't quietly over-refund a sale into a compliance problem. But it's also a rule you'd never know existed from reading a schema, and it changes what your own code needs to guard.

4. A credential has a shape, and the shape matters

Our very first real submission came back: invalid security key. The same key had worked minutes earlier from a manual test.

The key hadn't changed. What changed was what was around it — how the credential was stored on our side versus what the authority expected on the wire. A wrapper that was invisible in one path and fatal in another.

Boring bug. Ten-minute fix. But it happened at the last possible layer, after everything else was correct, and no amount of re-reading the specification would have surfaced it.

What I actually took from this

None of these are criticisms of GRA's documentation. It's more thorough than most public API specs I've read, and its test environment — with working credentials, published openly — is the reason all four findings cost hours instead of months. That's a good authority to integrate with.

The lesson is more general: a specification tells you what a system accepts. Only the system tells you what it does. That gap is unavoidable, and it isn't a reason to write less careful code. It's a reason to build so the gap is cheap to discover.

Three things made that true for us:

Preserve what actually happened, verbatim. Every submission stores the authority's raw response. All four findings came from reading exactly what came back, not from guessing what went wrong. Truncated logs would have cost days.

Classify unknowns conservatively. An error we didn't recognise had to mean stop and ask a human, not retry forever. Every one of these findings arrived as a clean halt with the authority's own words attached, instead of a loop or a corrupted record.

Make retrying safe by construction. In fiscalization the frightening failure isn't rejection — it's uncertainty. You submit, the network dies, and you don't know whether the authority stamped it. If retrying might create a second record, you're stuck. So one sale gets one identity, forever: submitting the same reference twice always resolves to the same transaction and the same number. Then a timeout becomes something you shrug at.

We tested that last one on purpose: cut the timeout so short that success was impossible, submitted a real invoice, watched it go unresolved — then let the system recover on its own. One number consumed. One certification. No duplicate. That test is worth more to me than any amount of specification-reading.

The honest status

Koolbase's Ghana support is verified end to end against GRA's test environment: sales, refunds, cancellations and purchase records all certified, plus the failure paths. Merchants onboard with GRA directly for production credentials — that relationship is theirs, as it should be, and we never sit between a taxpayer and their authority.

And the counter-case, because it's real: if you sell in exactly one country and already have a working integration, you don't need this. Compliance you've already built is compliance you already own.

What we're building for is the other case — the developer who hasn't built it, or who's about to enter a second country and discovered that the first integration doesn't transfer. Because underneath, this isn't a Ghana feature. It's an engine that treats a jurisdiction as a configuration: what the numbers are, what the documents contain, how the authority answers. Ghana is the first one that's live. It won't be the last.

Try Koolbase

Backend-as-a-service for Flutter and React Native, with over-the-air code push built in.

Start Building