Skip to main content
FBRDigital InvoicingERPPakistanBackend

FBR Digital Invoicing Inside a Multi-Tenant ERP: How We Built It

ZsTechLabs Team·September 18, 2026·8 min read

FBR digital invoicing looks simple from the outside: send each sales invoice to FBR, get a number back, print it. Once you build it into a multi-tenant ERP, where unrelated businesses share one application and each files under its own NTN, the details decide whether the integration is safe. This post walks through how we built FBR e-invoicing integration into FlowForg ERP: where the credentials live, how an invoice becomes FBR's payload, how sandbox scenarios gate going live, how filing retries avoid filing a sale twice, and what ends up printed on the receipt.

For the product view and the legal background, see our FBR digital invoicing software page.

What a business needs before FBR digital invoicing can work

No software can skip the steps FBR sets. The onboarding service in FlowForg follows the order FBR requires, which is:

  1. Register the ERP with FBR/PRAL and obtain a sandbox token.
  2. Post the test scenarios that apply to your business activity and sector until FBR accepts each one.
  3. Only then receive a production token and start filing live invoices.

On the FlowForg side, a workspace also needs a tax profile (NTN/CNIC, province and business address), and every product it sells needs an HS code and an FBR unit of measure.

We build the software. We do not give tax advice. Which tranche you fall into and when it applies is a question for FBR or your tax advisor.

Credentials: one encrypted token per workspace, per environment

FBR authenticates each request with a bearer token. That token is tied to one taxpayer, so the most serious failure possible in a multi-tenant system is serving one workspace's token to another. That would file one business's sales under someone else's registration.

The token is encrypted at rest and never serialised. The credential model casts the token as encrypted, so the database and any query log only ever hold ciphertext. It is also hidden from array and JSON output, because a credential that ends up in a log line or an exception payload has effectively leaked. The UI shows only a masked form. The HTTP client logs the workspace id, the endpoint and FBR's own status codes, never the token.

Every lookup names the workspace explicitly. Filing runs in queued jobs and console commands, where our usual tenant scope does nothing on purpose. So the credential store never relies on it. Every query filters on company_id directly.

There is a subtler rule too. FBR's sandbox and production endpoints differ only by a suffix, so the token, not the URL, decides which environment you are talking to. FlowForg stores the two tokens separately, and holding a production token does not switch a workspace to production. That is a separate, deliberate step:

// FbrOnboardingService
public function promoteToProduction(int $companyId): void
{
    if (! $this->canGoLive($companyId)) {
        throw new FbrRequestException(
            'This workspace cannot file live invoices yet: every required FBR scenario must pass and a production token must be stored first.',
            retryable: false,
        );
    }
    // ...set fbr_environment = production for this workspace only
}

Going back to sandbox is always allowed; only the dangerous direction is gated.

Mapping an invoice to FBR's payload

The mapper turns a FlowForg sales invoice into the JSON body FBR's post-invoice endpoint expects: the seller's NTN, name, province and address, the buyer's details and registration type, and one entry per line with HS code, rate, unit of measure, quantity, value excluding sales tax, sales tax and discount.

Two design decisions matter most.

Readable failures before FBR's codes. When FBR rejects something, it answers with a numeric error code against a line. A shop cannot act on that. So before it builds a payload, the mapper checks the invoice and reports problems in the shop's own terms: "Your NTN/CNIC is not set", "'Item X' has no HS code, so set it on the product", or "Customer is marked sales-tax registered but has no NTN/CNIC recorded." The filing service runs the same check before every post.

Money comes from what was stored, not recalculated. The line values are the tax-inclusive total the customer was charged and the tax the ledger recorded. The ex-tax value is the difference between them. The rate string FBR wants ("18%") is worked out from those stored amounts, not from the product's nominal tax group, because a POS override or a promotion can move the effective rate. The filing has to match the receipt.

A return is filed as a Debit Note quoting the original sale's FBR number; if the original is not filed yet, the mapper says so instead of sending something FBR will reject.

The scenario catalogue: prove what your business actually does

FBR's sandbox asks you to post sample invoices for a set of scenarios (SN001 to SN028 in the PRAL DI API specification): standard-rate sales to registered and unregistered buyers, reduced-rate, exempt and zero-rated sales, 3rd-schedule goods, sales to end consumers by retailers, and so on.

The required set depends on your business activity and sector, so we transcribed the spec's applicability matrix into FbrScenarioCatalogue. A retailer in the Wholesale/Retails sector proves four scenarios, while a manufacturer in the same sector proves fifteen. Sending a shop through the manufacturer's list would fail it on scenarios FBR never asked it to prove. The retailer-only scenarios (SN026 to SN028) are also dropped for a workspace that is not registered as a retailer, because FBR will not accept them from that taxpayer.

For each required scenario, a sample builder creates a synthetic invoice. It uses the workspace's own real seller identity, because FBR checks it, plus a representative line for that scenario. It posts only to the sandbox, and the onboarding service refuses to run scenarios against production. A workspace goes live only when every required scenario has passed and a production token is stored.

A filing service built around "no idempotency key"

This is the part that needs the most care. FBR's digital invoicing API has no idempotency key. If you post the same sale twice, it is filed twice, and undoing that takes a debit note and a conversation with a tax adviser. So the filing service is built around one question: did this failure happen before the request reached FBR, or after?

  • Invoice posts are never retried automatically by the HTTP client. Read-only lookups are retried; writes are not.
  • The sale is marked "sending" and saved before the request leaves. If a worker crashes mid-post, a trace remains. Otherwise a crash would look exactly like "never tried", and the next sweep would file the sale again.
  • Outcomes are recorded, not thrown. Each attempt ends as filed, failed (safe to retry), unknown (may have reached FBR) or skipped. Every attempt is logged, with the exact payload kept for failures.

Pending filings are then worked off by a scheduled command, fbr:file-pending, in two passes:

  1. Resolve stale sends. Any sale stuck in "sending" past a threshold (15 minutes by default) becomes unknown. No automatic path ever touches it again. It waits for a person who has checked FBR's own records.
  2. Queue what is genuinely owed, with a backoff by attempt number: 2, 10 and then 60 minutes. A sale that has used all its attempts stays on the FBR Filing screen for someone to look at. A return waits until its original sale has an FBR number.

The job that does the posting runs with $tries = 1 and a unique lock per sale, so the queue can never retry a post that timed out, and an overlapping sweep can never start a second post of the same invoice. Automatic filing is also off by default. It is a switch an operator turns on knowingly, while manual filing from the FBR Filing screen works either way.

The invoice QR code and print marks

The printed invoice gets an FBR block, and it has three possible states:

  • Nothing. A workspace not filing to FBR prints no FBR block at all. That includes sandbox workspaces: a sandbox number belongs to a test registration, so printing FBR's logo or a QR from it would put a compliance claim on a real customer's receipt that FBR has no record of.
  • Pending. The workspace files live, but this sale has no FBR number yet. The receipt says so plainly instead of leaving a blank.
  • Filed. The FBR logo, the QR code and the invoice number FBR issued.

The QR code follows the printing section of the spec: Version 2 (25×25), one inch square. It encodes the FBR invoice number and nothing else. At error-correction level M, a Version 2 symbol has room for that number and no more, and it is the value FBR's own verification looks up. We encode in alphanumeric mode, which packs FBR's uppercase-and-digit numbers tightly enough to stay inside Version 2.

Two printing details mattered:

  • The QR is rendered as SVG, not a bitmap. Receipts print from the browser at whatever DPI the thermal printer uses, and resampling a raster QR is how QR codes stop scanning.
  • The quiet zone is drawn inside the image, so the image is sized larger than one inch in order to make the symbol itself one inch. Sizing the image at exactly one inch would print a symbol noticeably under spec.

If the QR ever fails to render, the receipt still prints with the number and the failure is reported. The sale has already happened and the customer is waiting. The same marks apply to POS till receipts and to printed sales invoices.

Why this lives inside the ERP

In FlowForg, the invoice filed to FBR is the same invoice that moved your stock, posted to your ledger and settled against the customer's account. There is no second system to reconcile.

If you are a sales-tax-registered business in Pakistan looking at FBR POS integration or invoice filing, see the FBR digital invoicing software page for the full workflow, or explore FlowForg ERP. The best way to evaluate it is a demo on your own invoice shapes, starting in the sandbox.