A hospital finance team wanted their billing and pharmacy numbers in a dashboard. The data existed — every bill was sitting in the management platform they had been using for years. The platform had a reports screen. What it did not have was any way to get those numbers out on a schedule, into anything else.
So every morning someone signed in, set the date filter, ran the report, downloaded it, and pasted it into a spreadsheet. That is a person spending twenty minutes a day being an API.
This situation is extremely common, and the way out is a ladder. Start at the top. Only go down a rung when the one above is genuinely blocked.
Rung 1 — Ask for database access
Boring, and it works more often than people assume. Many on-premise systems run on a database sitting on a server in the same building, and the vendor will grant a read-only user if the client asks as the owner of the data rather than as a favour.
If you get this, stop reading. A read-only replica is the cleanest integration that exists — no scraping, no fragility, no surprises when the vendor changes a screen.
You will be refused when the platform is hosted by the vendor, when the schema is treated as a trade secret, or when nobody at the vendor is empowered to say yes. Which is often.
Rung 2 — Find the API the app is already using
Here is the thing worth internalising: if a web application shows you a table, an API returned that table. The reports screen that renders 75 bills fetched those bills from somewhere. It may be undocumented, but it exists.
Open the browser's developer tools, go to the Network tab, and run the report the way a user would. Watch what fires. You are looking for the request whose response contains the rows you can see on screen. Then check:
- What authentication it carries — a session cookie, a bearer token, a custom header
- Which parameters are the filters, and what happens when you widen the date range
- Whether it paginates, and what the page size limit really is
- Whether the response is the raw data or already formatted for display
On the hospital platform this worked. The reporting API was there, undocumented but perfectly usable, and the filters mapped cleanly onto query parameters. Half the job was done.
Do this part properly
Two rules that keep this legitimate and keep it working:
- Use the client's own credentials, with the client's authorisation. You are automating a login that a staff member already has and already uses for this exact report. That is the boundary — you are not getting around a permission, you are getting around a missing feature.
- Be gentler than a human. One request an hour, not a hammering loop. If your integration is a load problem for the vendor's server, you have built the wrong thing.
Rung 3 — Drive the browser
Sometimes the API gets you 90% of the way and then stops. That is what happened here: the report data came back fine, but the actual file the report generates lives in a storage layer that refuses anything except a real, signed, in-session browser request. No amount of header copying satisfied it.
At that point the honest answer is to stop imitating a browser and just use one. A headless browser — Playwright, in this case — signs in with the client's credentials, navigates to the report, applies the same filters a person would, clicks download, and hands over the genuine file.
It is slower and more fragile than an API call, and it needs re-checking whenever the vendor changes the screen. But it is the same actions a staff member was performing by hand, so it is guaranteed to be possible, and it produces the platform's own file rather than your reconstruction of it.
Rung 4 — Scheduled report email, or the file drop
The last resort, and still better than a human. Some platforms will email a report on a schedule even when they will not export one. Point that email at an inbox you control, parse the attachment, load it. Ugly, but it runs without anyone remembering to do it.
Getting it in is only half the job
Whichever rung you land on, the pipeline behind it needs three properties or it will quietly poison the dashboard it feeds:
- Idempotent writes. Every run must be safe to run again. Upsert on a natural key — bill number plus line number, say — so re-running an hour twice produces the same rows, not double the revenue.
- A run log. Every execution writes a row: when it started, what range it pulled, how many rows landed, whether it failed and why. The first question anyone asks about a number is "is this current?", and without a log you are guessing.
- Reconciliation. Before anyone trusts the dashboard, take one period, export it manually from the platform the old way, and compare row by row and column by column against what your pipeline produced. Not a spot check — every monetary column.
An integration nobody has reconciled is a rumour with a database behind it.
For the hospital pipeline that meant a full match across 75 bills and every money column, checked against the platform's own export. That number is what moved finance from running the manual report "just to be sure" to not running it at all.
The honest caveats
Undocumented APIs change without notice, so the pipeline needs monitoring — a failed run has to page someone, not fail silently. Browser automation breaks when a screen is redesigned. And none of this substitutes for pushing the vendor for a proper integration; it buys you working numbers while that conversation goes nowhere.
But the alternative — a person copying numbers every morning — has failure modes too. They are just harder to see.
The full write-up of this build, including the schema-agnostic explorer that came out of it, is in the case studies. If your data is trapped somewhere similar, describe the screen and I will tell you which rung it is on.