Documentation
Provenance and freshness: the response headers
Every Triguna response carries where the record came from and how old it is, in headers rather than in the body. How to read them and how to control freshness.
On this page
Every successful response tells you where the record came from and how old it is. That information travels in response headers, not in the JSON body.
What you get
| Header | Values | Meaning |
|---|---|---|
| X-Data-Source | live | store | stale_fallback | Whether this call hit the upstream provider, was served from our stored copy, or fell back to a stored copy because upstream was failing. |
| X-Fetched-At | ISO 8601 UTC | When the upstream provider was last hit for this record. |
| X-Data-Age-Seconds | integer | How old the record is, in seconds. 0 on a live fetch. |
| X-RateLimit-Limit | integer | Requests per second granted to your account. |
| X-RateLimit-Remaining | integer | Tokens left in your bucket right now. |
curl -i -H "ApiKey: $TRIGUNA_API_KEY" \
"https://api.triguna.ai/v1/people/profile?profile_id=williamhgates"
HTTP/1.1 200 OK
Content-Type: application/json
X-Data-Source: live
X-Fetched-At: 2026-07-31T14:02:11+00:00
X-Data-Age-Seconds: 0
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
Reading them:
const res = await fetch(url, { headers: { ApiKey: KEY } });
const record = await res.json();
const provenance = {
source: res.headers.get('X-Data-Source'), // live | store | stale_fallback
fetchedAt: res.headers.get('X-Fetched-At'),
ageSeconds: Number(res.headers.get('X-Data-Age-Seconds') ?? 0),
};
Controlling freshness
Both enrichment endpoints accept max_age_days.
| Parameter | Type | Behaviour |
|---|---|---|
| max_age_days | number | Accept a stored copy up to this many days old. 0 means always fetch live. Your value is capped by the platform ceiling, currently 7 days. |
| bypass_cache | boolean | Force a genuinely fresh fetch, skipping both our store and the provider cache. |
The current default is live. Every call fetches from upstream unless you ask otherwise, and
the result is stored with its fetched_at.
# Live, the default.
curl -H "ApiKey: $KEY" ".../v1/people/profile?profile_id=williamhgates"
# A day-old copy is fine, so serve from the store when one exists.
curl -H "ApiKey: $KEY" ".../v1/people/profile?profile_id=williamhgates&max_age_days=1"
A stored hit is faster and does not touch the upstream provider. Whether it consumes a credit is
a platform setting (charge_on_cache_hit), so treat a store response as billable unless told
otherwise for your account.
Staleness fallback
If the upstream provider fails transiently and we hold any copy of the record, we serve that copy
with X-Data-Source: stale_fallback rather than returning an error. Your integration keeps
working through a vendor outage for every entity seen before.
if (res.headers.get('X-Data-Source') === 'stale_fallback') {
// Upstream is degraded. The record is real but may be out of date;
// check X-Data-Age-Seconds before acting on time-sensitive fields.
}
A profile that was never found does not fall back. Not-found stays not-found.
Using it well
Log the headers with any decision the record drove. If a record routed a signup, store
X-Data-Source and X-Fetched-At on the routing event. Six weeks later “the data was wrong”
becomes a question you can answer.
Treat X-Fetched-At as the age of the claim, not the age of the fact. A record fetched a
second ago can still describe a job someone left last month. Freshness of retrieval and freshness
of truth are different things, and only the first is something an API can promise.
Set max_age_days per call site, not globally. A nightly scoring job is usually happy with a
week-old copy. A record shown to a user mid-conversation probably is not.
A minimal audit shape
create table enrichment_events (
id bigserial primary key,
entity_key text not null, -- member_urn or company_id, never the slug
decision text not null,
data_source text not null, -- X-Data-Source
fetched_at timestamptz not null, -- X-Fetched-At
raw_payload jsonb not null, -- keep it; re-fetching costs a credit
created_at timestamptz not null default now()
);
Keying on member_urn or company_id rather than the slug matters. See
identifiers and slugs for why slugs break joins.