A retailer's product page answered with status 200 and nothing in it. The pipeline didn't log a block. It logged an empty shelf.
That's the failure nobody budgets for in alternative data. Not a missing dataset, not a slow vendor. A collection layer that keeps handing you rows while quietly measuring something other than the company you're covering.
The Challenge #
In January 2026, Exabel surveyed 100 fundamental portfolio managers and analysts across the US, UK, Singapore and Hong Kong, managing roughly $610 billion between them. 71% named combining data from different sources as the most frustrating part of working with alternative data, and 94% said they already run AI or machine learning somewhere in their research process.
Read those two numbers next to each other and the shape of the problem shows up. The modeling side is well staffed. The plumbing underneath it isn't.
A web-collected panel (prices, stock status, careers pages, review counts, marketplace assortment) is a time series before it's anything else. Every time series carries an assumption that nobody writes into the spec: today's observation was collected the same way yesterday's was. Break that assumption loudly and you get an alert. Break it quietly and you get a signal.
Three of those quiet breaks show up in almost every web-collected panel.
The 200 that isn't content. Bot defenses stopped answering with a clean 403 a long time ago. A challenge page, a consent wall, or an empty-results template arrives with a success status and a body your parser reads as zero results. Fewer listings looks identical to less demand.
The vantage point moved. Price, currency, assortment, promotional banner, sometimes whether the page renders at all: retailers decide all of it from where the request appears to come from. If Monday's collection exited in Germany and Thursday's exited in Poland, there's a step in your series that belongs to you, not to the retailer.
The rotation that took the first answer it got. Rotate through exits without telling the collector what success means, and it stops at the first exit that responds. A refusal is a response. So the row lands, the job goes green, and nobody looks again.
None of these throw an exception. Ingestion counts rows, the dashboard stays green, and the analyst gets a chart. Then the model spends a quarter learning the behavior of your collection infrastructure instead of the behavior of the business.
The Approach #
Treat data integrity as a property of the request, not of the parser downstream. Three things have to be true.
1. Say what a real page looks like. The collector has no way to work it out on its own. Give it a string that only genuine content carries and a couple that only refusals carry, and a challenge page stops counting as an observation. We wrote about this when validate rules landed: the request itself decides what success means.
import requests
r = requests.post(
"https://api.foura.ai/api/proxy",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"maxTries": 8,
"exitCountries": ["DE"],
"request": {
"method": "GET",
"url": "https://retailer.example/p/12345",
"validate": {
"status": {"accept": [200]},
"data": {
"accept": ["data-testid=\"price\""],
"fail": ["Access Denied", "Just a moment"]
}
}
}
}
).json()
observation = r["data"] # content your rules accepted, or nothing
exit_id = r["proxy"] # opaque ID of the exit that delivered it
served_from = r["exitCountry"] # verify it against what you asked for
Rotation now has a definition of done. It keeps trying exits until one returns something that passes your rules, instead of returning the first page-shaped object it meets.
2. Hold the vantage point still. exitCountries
is a strict allowlist of target-visible country codes, and exits with unknown geography are left out rather than substituted. Two honest caveats, both worth knowing before you build on it. Country metadata is refreshed on a cycle (normally within about ten minutes), so it isn't a live lookup at request time, which is exactly why the response carries exitCountry
for you to check. And when the pool has no match for the scope you asked for, the call comes back with HTTP 200 and an error envelope rather than an exception. Read the body, not the status. Preserve the scope and retry later instead of widening it, because a widened scope is a break in the series.
3. Keep the identity of the exit. The proxy
field is an opaque ID, not an address. Pass it back on a follow-up Single or Browser call and the detail page comes from the same vantage point as the search page that found it (how to reuse an exit). Store that ID and the X-FourA-Request-Id
header alongside every row. When an analyst questions a spike six weeks later, the question "was this real?" becomes a lookup instead of an argument.
For a source you're onboarding and don't understand yet, Auto is the fastest way to find a working path: it walks the cheap-to-expensive ladder, tells you which rung won, and hands back the session that worked. Use it to find the route, then put production volume on the direct engines. Replay that session through Single where nothing needs rendering, Browser where it genuinely does. Path finding and steady-state collection are different jobs.
Results #
What changes when those three properties hold, on a panel of a few thousand product pages a day across a dozen retailers (illustrative scenario based on industry benchmarks):
A gap is a gap, not a guess. Refusals never enter the panel as zeros, so an empty result set means the retailer showed nothing, and your absence signal is worth trading on.Comparable rows. Every observation in a series comes from the country the series is defined in, so a price move is a price move.Reproducible history. Exit ID plus request ID per row means any disputed data point can be traced back to the exact call that produced it.Cheaper per usable row. Requests that would have produced a discarded observation get retried at collection time rather than paid for, parsed, stored, and later cleaned out of a backtest.
But the last one is what research teams underrate. A bad row isn't free just because it was cheap to fetch. It costs a research cycle, and sometimes it costs the confidence of the person who has to sign off on the signal.
Key Takeaway #
Alt data buyers evaluate providers on coverage, latency, and history depth. Almost nobody asks the question that decides whether a panel is tradeable: how does this dataset behave on the day the source refuses to answer?
A vendor that drops the row is honest. A vendor that returns a courtesy page as an observation has sold you a measurement of their own infrastructure, and you'll pay for it in a backtest that works right up until it doesn't. That question belongs in every data diligence checklist, and it belongs in your own pipeline first, because if you're collecting it yourself, you're the vendor.