Case Study · API Reverse-EngineeringGovTech / KYC

Any Pakistani taxpayer, verified in a single call

FBR's IRIS portal shows a taxpayer's active-filer status — behind what looks like a captcha and a browser-only interface. This project turned that manual, one-lookup-at-a-time process into a clean programmatic call, by reading FBR's own frontend closely enough to realize the lock was never really locked.

AM

Arham Mirkar

DataLayer — Enterprise Data Infrastructure

1 call
Verification in a single request
0
External dependencies
4
ID types: CNIC, NTN, Passport, Reg
50
Concurrent workers in bulk mode
The Problem

Verification that only works one human at a time

Lending apps, HR teams, and anyone running KYC needs to confirm a person's tax status with FBR. The data is public — but it's served through a portal that expects a human: type an ID, solve a captcha, read the result off the screen, repeat.

That's fine for one check. It falls apart the moment you need to verify a list of a few hundred borrowers, or wire verification into an onboarding flow. The question wasn't whether the data could be automated — it was whether the captcha was a real barrier or just a costume.

The Investigation

Five moves from portal to plain API

This one was less about code and more about looking carefully. The whole solution is small; the value was in the detective work that made it possible.

Capture

Network export

Recorded a full Chrome network export of a real verification on the IRIS site, then wrote a small script to trim gigabytes of traffic down to the handful of FBR calls that actually mattered.

Locate

Read the frontend

Found the endpoint and an application Bearer token hardcoded in IRIS's own Angular bundle — base_verification_url and authorization_key_verifcation (FBR's own spelling) — sitting in plain sight in the minified JavaScript.

Defeat the captcha

The key insight

The captcha looked like the blocker. Reading the bundle revealed it is generated and checked entirely in the browser and never sent to the server — so it simply doesn't exist from the API's point of view.

Reconstruct the request

Payload schema

Network exports don't capture request bodies, so the exact field names — protocolId, outputType, identifierType, identifier, date — were recovered from the bundle's submit function and confirmed against live responses.

Speak JSON, not SOAP

Content negotiation

The same endpoint returns SOAP XML by default; sending the right Accept header flips it to clean JSON. A small detail that turned a parsing nightmare into a one-line extraction.

Deep Dive 01

One request, done right

The final call is deliberately unremarkable — which is the point. It carries the application token from the frontend, asks for JSON, and sends the identifier and today's date.

POST https://api.fbr.gov.pk/iris2ovs/v1/getdata
Authorization: Bearer <application-jwt>
Accept: application/json      # without this, FBR replies in SOAP XML

{
  "protocolId": "1004",
  "outputType": "4",
  "identifierType": "CNIC",   # or NTN / Passport No. / Reg/Inc. No.
  "identifier": "42201XXXXXXX",
  "date": "05,Aug,2026"
}

The endpoint and token were never meant to be found — they live inside FBR's own Angular bundle, which the browser downloads on every visit.

Deep Dive 02

Turning HTML-in-JSON into clean fields

FBR returns the answer as a blob of HTML wrapped in JSON. The client parses the labels out of that markup into a tidy, typed object ready to drop into a database or KYC flow.

What comes back

registrationNoThe CNIC / NTN that was checked
nameRegistered taxpayer name
businessNameBusiness name, when applicable
filingStatuse.g. Active (Late Filer)
filingStatusCheckingDateDate the status was verified

Parsed result

{
  "registrationNo": "42201XXXXXXX",
  "name": "—",
  "businessName": "",
  "filingStatus": "Active (Late Filer)",
  "filingStatusCheckingDate": "05,Aug,2026"
}

From a captcha-gated screen to a structured record your systems can actually use.

Deep Dive 03

Built for one — or a whole list

The single lookup is a zero-dependency script. On top of it sits a bulk runner designed for large CSVs — because real KYC work is a spreadsheet of thousands of IDs, not one at a time.

Concurrent worker pool

Streams a CSV of identifiers, de-duplicates them, and runs lookups through a configurable worker pool (50 by default) instead of one-at-a-time.

Retries + resume

Three retries with exponential backoff and jitter on failure, and a resume flag that continues from an existing output file so a long run is never lost.

Live progress

Prints throughput, ETA, and error counts every couple of seconds, and writes a clean CSV with a per-row error column for auditing.

AM

Arham Mirkar

Founder & Data Engineer, DataLayer

“My favorite part of this one is that there's almost no code. I spent a day reading someone else's minified JavaScript, found that the captcha was theater, and the actual client fit on a page. Good reverse-engineering usually makes the final solution look obvious.”

Under the Hood

Technology

LayerApproach
DiscoveryChrome net-export capture, custom log-trimming scripts, Angular bundle analysis
The insightClient-side-only captcha, application Bearer JWT lifted from the frontend, JSON vs SOAP content negotiation
ClientZero-dependency Node.js (native fetch), single-lookup CLI, regex field extraction
BulkStreaming CSV, 50-worker concurrency, 3× retry with backoff + jitter, resumable output
MaintenanceToken-refresh helper that re-scrapes the live IRIS bundle when FBR rotates the key

This is the engine behind DataLayer's Tax Verification API — the productized, hosted version of the same capability.

Got a system that's locked behind a screen?

If a human can pull the data through a browser, it can usually be turned into a clean, programmatic integration. Let's find the door.