Source Provenance and Caching
Response headers that show which document a result came from, when it was fetched, and whether it was cached.
When an extraction feeds a decision someone may later need to defend, the result alone is not enough. You also need to show which document produced it and whether it was fresh. Every successful response from /extract/markdown, /extract/json and /generate/json carries that information in response headers, so it never changes the shape of your result.
The headers
Section titled “The headers”| Header | Example | Meaning |
|---|---|---|
X-Tabstack-Final-Url | https://www.mozilla.org/ | The URL the source document was served from, after redirects. Can differ from the url you sent. |
X-Tabstack-Origin-Status | 200 | The HTTP status the origin returned for that document. |
X-Tabstack-Retrieved-At | 2026-11-04T17:52:38Z | When the document was retrieved from the origin, as an RFC 3339 UTC timestamp. |
X-Tabstack-Checksum | sha256:17f6426d…48fa7 | SHA-256 of the source document the response was extracted from. See What the checksum covers. |
X-Tabstack-Cache | miss | How caching produced the response. See Cache status. |
Age | 3 | Seconds since the cached content was stored. Sent only when cached content was served. |
X-Trace-Id | a47d0ac0e16e96b2613867f231da10b6 | The identifier for this request. Quote it when you contact support. Also sent on error responses. |
A response looks like this:
HTTP/1.1 200 OKContent-Type: application/jsonX-Tabstack-Cache: missX-Tabstack-Checksum: sha256:17f6426dbd14b08ef0bc1fb5c32c8f34461319038841e27e42468534db448fa7X-Tabstack-Final-Url: https://www.mozilla.org/X-Tabstack-Origin-Status: 200X-Tabstack-Retrieved-At: 2026-11-04T17:52:38ZX-Trace-Id: a47d0ac0e16e96b2613867f231da10b6Here the request was for http://mozilla.org. The final URL shows the page that was actually read.
What the checksum covers
Section titled “What the checksum covers”The checksum is computed over the exact bytes your result was extracted from, which depends on how the page was fetched:
- A plain fetch, such as a PDF or a static page, hashes the file as downloaded. If you hold a copy of the file, its SHA-256 matches the checksum when it is the same document Tabstack read.
- A page rendered in a browser (typical at
effort: "max") hashes the rendered page. Scripts, ads and timestamps mean a rendered page can differ on every load, so two fetches of the same URL may produce different checksums even when the visible content is unchanged.
Downloading the page yourself later and comparing hashes proves little on its own, because the source can change between fetches. The checksum is most useful recorded alongside the result at the time of extraction: it identifies which bytes produced that result.
Cache status
Section titled “Cache status”Tabstack keeps two caches: one for fetched pages and one for finished responses. X-Tabstack-Cache tells you which, if either, was used:
| Value | Meaning |
|---|---|
miss | The page was fetched and processed fresh. |
fetch-hit | The page came from the fetch cache, and the response was produced fresh from it. |
hit | The whole response was served from the response cache. |
bypass | You set nocache: true, so no cache was read. |
With hit and fetch-hit, X-Tabstack-Retrieved-At is the time the page was originally retrieved, not the time the cached response was served, and the checksum is that of the original document. Age tells you how long ago the content was stored.
When you need a record that the page was read at a specific moment, pass nocache: true. The response then reports bypass and a Retrieved-At of the current request. See Production Reliability for when to bypass the cache.
When a header is missing
Section titled “When a header is missing”A header is left out rather than guessed when its value is unknown. In particular, Tabstack never fills in the final URL with the URL you requested.
X-Tabstack-Final-Url: not every fetch method can see the URL a page was ultimately served from.X-Tabstack-Origin-Status: a browser fetch that timed out before the page finished loading, but still had usable content, has no status to report.- Any provenance header on a cached response: responses cached before a header was introduced are served without it until they expire.
Treat each header as optional in your code.
Reading the headers
Section titled “Reading the headers”import Tabstack from "@tabstack/sdk";
interface Provenance { finalUrl: string | undefined; originStatus: number | undefined; retrievedAt: string | undefined; checksum: string | undefined; cache: string | undefined; traceId: string | undefined;}
// Every header can be absent, so every field may be undefined.function provenanceFrom(headers: Headers): Provenance { const get = (name: string) => headers.get(name) ?? undefined; const status = get("X-Tabstack-Origin-Status"); return { finalUrl: get("X-Tabstack-Final-Url"), originStatus: status ? Number(status) : undefined, retrievedAt: get("X-Tabstack-Retrieved-At"), checksum: get("X-Tabstack-Checksum"), cache: get("X-Tabstack-Cache"), traceId: get("X-Trace-Id"), };}
const client = new Tabstack();
// withResponse() returns the raw response alongside the parsed result.const { data, response } = await client.extract .markdown({ url: "http://mozilla.org" }) .withResponse();
// Keep the provenance next to the result it describes.const record = { content: data.content, provenance: provenanceFrom(response.headers),};from typing import Optional, TypedDict
import httpxfrom tabstack import Tabstack
class Provenance(TypedDict): final_url: Optional[str] origin_status: Optional[int] retrieved_at: Optional[str] checksum: Optional[str] cache: Optional[str] trace_id: Optional[str]
# Every header can be absent, so every field may be None.def provenance_from(headers: httpx.Headers) -> Provenance: status = headers.get("X-Tabstack-Origin-Status") return { "final_url": headers.get("X-Tabstack-Final-Url"), "origin_status": int(status) if status else None, "retrieved_at": headers.get("X-Tabstack-Retrieved-At"), "checksum": headers.get("X-Tabstack-Checksum"), "cache": headers.get("X-Tabstack-Cache"), "trace_id": headers.get("X-Trace-Id"), }
client = Tabstack()
# with_raw_response returns the raw response; parse() gives the result.response = client.extract.with_raw_response.markdown(url="http://mozilla.org")data = response.parse()
# Keep the provenance next to the result it describes.record = { "content": data.content, "provenance": provenance_from(response.headers),}tabstack extract markdown http://mozilla.org -p-p (--provenance) prints the provenance to stderr after the result, so piped output is unchanged:
provenance final url https://www.mozilla.org/ origin status 200 retrieved 2026-11-04T17:52:38Z checksum sha256:17f6426dbd14b08ef0bc1fb5c32c8f34461319038841e27e42468534db448fa7 cache miss trace id a47d0ac0e16e96b2613867f231da10b6With --output json, it is a single {"provenance":{...}} line on stderr instead.
curl -si https://api.tabstack.ai/v1/extract/markdown \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"http://mozilla.org"}' | grep -i '^x-tabstack\|^age\|^x-trace-id'The headers are exposed to browsers through CORS, so code running in a web page can read them too.