Engenharia de Software 4 min min read 2 views

A webhook is not a source of truth: what I learned integrating electronic tax invoices

E
Eduardo Piasson
06 Oct 2026
A webhook is not a source of truth: what I learned integrating electronic tax invoices

The problem: the answer does not come right away

In Oficina Simples, the repair shop issues invoices for parts (NF-e) and for services (NFS-e) straight from the work order. Issuing goes through an intermediary that talks to the state tax authority and to city governments.

Almost everything there is asynchronous. You send the invoice and get back a "processing" status. The authorization, or the rejection, arrives minutes later through a webhook.

The temptation is obvious: receive the webhook, read the status from the body and write it to the database. It works on the first test. And it is exactly the kind of code I did not want in production.

Rule 1: the webhook is a nudge, not the answer

The endpoint that receives the notification does four things, in this order:

  1. Checks a per-company secret in the header, using a constant-time comparison.
  2. Looks up the invoice by its reference, within the company in the URL.
  3. If the reference does not exist, logs it and returns 200. It might be an invoice issued outside the system or from another environment, and there is no point making the sender retry.
  4. If it exists, queries the invoice at the source and only then updates the status.

The webhook body is not used for anything except the reference. The consequence is that a forged POST cannot authorize any invoice: at most, it triggers a lookup. The header secret is not the main defense, it only prevents pointless lookups.

This pattern applies to any integration: payments, shipping, e-signatures. Use the webhook to learn that something changed. Ask the source what changed.

Rule 2: notifications get lost, so someone has to check

Webhooks fail. The server was mid-deploy, the network flickered, the sender gave up after a few retries. If the system depends only on the notification, the invoice stays in "Processing" forever.

So there is a sweep that runs every minute and checks pending invoices with a growing interval:

  • In the first 30 minutes: every 1 minute.
  • Up to 6 hours: every 15 minutes.
  • After that: hourly, for up to 3 days.

Each invoice stores when it was last checked, the sweep takes at most 100 per run, and a failure on one invoice does not take the others down. On screen, the list refreshes itself every 10 seconds while any invoice is processing.

The webhook makes the experience fast. The sweep makes the system correct.

Rule 3: validate before calling whoever is going to reject you

A rejection from the tax authority comes with a code and a message that no shop owner should have to learn to read.

Before any issuance, the system checks everything that would be rejected: the shop's tax ID and tax regime, an expired certificate, the parts' product classification codes, the customer's document and address. The answer comes back as a plain-language list that points to where to fix it, such as "Enter the shop's tax ID under Company Details".

It is more code than just passing the error along. But it is the difference between one support ticket a day and none.

Rule 4: store as little as possible

The A1 digital certificate is the most sensitive credential in the whole operation. In reseller mode, the file is not stored: it goes straight to the intermediary and the temporary copy is deleted right after. The system keeps only the tokens, encrypted, and the expiry date, which feeds reminders 30, 15, 7 and 1 day ahead.

You cannot leak what you never stored.

Rule 5: what is not covered should be blocked, not guessed

Taxes have too many edge cases. One of them, selling to an end consumer in another state under certain tax regimes, I decided not to cover in this version.

The easy way out would be to issue with an approximate rule. The way I chose: block it with a clear warning to issue through the accountant. A wrong invoice costs far more than an invoice the system did not issue.

Rule 6: be honest about what was tested

The integration was written from the official documentation and tested against a simulated API. That is not the same as issuing for real.

So the documentation has an explicit certification checklist with what can only be confirmed with a real tax ID and certificate: the city's service tax rate format, the correction letter response, the fields from the upcoming tax reform. And issuing starts switched off for everyone, enabled by a toggle in the admin panel, with no deploy.

Tax integration is not the place for "it worked on my machine". It is the place to distrust every response until the source confirms it.

Newsletter

New articles straight to your inbox.

✓ Check your email to confirm your subscription.

Related posts