Person Profile API (v3)
How the v3 Person Profile endpoint works — identifiers, freshness tiers, stale fallbacks, response metadata, billing, and error handling.
The v3 Person Profile endpoint (GET /api/v3/person) looks up a person by a profile URL or another supported identifier and returns the enriched v3 person profile. Compared to v2, it replaces cache parameters with explicit freshness tiers, reports a freshness verdict on every response, and returns structured, machine-readable errors.
Making a request
Send a GET request to https://enrichlayer.com/api/v3/person with your API key in the Authorization header as a Bearer token, and exactly one identifier parameter:
| Parameter | Description |
|---|---|
profile | A person profile URL to look up, in the form https://linkedin.com/in/example-user/. |
email | An email address to look up (any address known for the person). |
github | A GitHub profile URL or username to look up. |
facebook | A Facebook profile URL to look up. |
twitter | A Twitter/X profile URL to look up. |
A request without an identifier is rejected with a 400 and the error code invalidInput.
curl -G \
-H "Authorization: Bearer ${ENRICHLAYER_API_KEY}" \
'https://enrichlayer.com/api/v3/person' \
--data-urlencode 'profile=https://linkedin.com/in/example-user/' \
--data-urlencode 'freshnessTier=fresh'Choosing a freshness tier
Instead of v2's cache parameters, v3 lets you buy a freshness guarantee with the freshnessTier parameter:
| Tier | Guarantee |
|---|---|
live | Forces a live source fetch. A cached profile is never returned as a success. |
fresh | The profile was validated within the last 7 days. |
recent | The profile was validated within the last 15 days. |
standard | The profile was validated within the last 30 days. This is the default. |
cached | Cache-only — never triggers a live fetch. The profile is guaranteed to be no older than 90 days. |
If the cached profile already satisfies the requested tier, it is returned immediately. If it doesn't, Enrich Layer performs a live fetch to meet the guarantee.
Pick the loosest tier your use case tolerates: looser tiers are served from cache more often, which means faster responses and lower cost.
Falling back to stale data
By default, when you request fresh, recent, or standard and the live fetch fails, the request fails with a 503 or 504. Set allowStale=true to receive a clearly labeled stale profile instead — the newest cached profile, within the 90-day cached cap. allowStale defaults to false.
Two things allowStale does not do:
- It does not override a
404. If the source reports the profile gone or inaccessible, you get the404— not stale data. - It has no effect with
freshnessTier=cached, which never fetches in the first place.
Combining allowStale=true with freshnessTier=live is invalid and returns a 400 with the error code invalidFreshnessPolicy — live means "never a cached result", so a stale fallback would contradict the request.
Reading the freshness verdict
Every 200 response carries a meta.freshness object with the verdict and a meta.billing object with the charge. Branch on meta.freshness.met, not on the profile age:
| Field | Description |
|---|---|
meta.freshness.tier | The freshness tier you requested. |
meta.freshness.met | The primary machine-readable verdict: whether the delivered profile met the requested tier. |
meta.freshness.profileAgeDays | Profile age in whole days, rounded up. 0 means the profile was served live. |
meta.freshness.fallback | Present only when stale data was returned because allowStale=true. Always "stale". |
meta.billing.chargedTier | The tier actually charged. Lower than the requested tier when a stale fallback was delivered. |
meta.billing.creditsCharged | The number of credits charged for this response. |
A request that met its tier reports:
{
"tier": "standard",
"met": true,
"profileAgeDays": 12
}A stale fallback is labeled met: false with fallback: "stale", and the response also carries a meta.warnings entry with the code servedStaleFallback:
{
"tier": "fresh",
"met": false,
"profileAgeDays": 42,
"fallback": "stale"
}meta.warnings is a human-readable notice list — the meta.freshness object is the machine contract.
Response headers
Four response headers mirror the verdict, so you can route on it without parsing the body. The body is canonical; the headers are a convenience.
| Header | Mirrors |
|---|---|
EnrichLayer-Freshness-Tier | meta.freshness.tier |
EnrichLayer-Freshness-Met | meta.freshness.met |
EnrichLayer-Profile-Age-Days | meta.freshness.profileAgeDays |
EnrichLayer-Charged-Tier | meta.billing.chargedTier |
Every response also carries an x-request-id header — quote it when reporting issues.
How credits are charged
Billing follows the delivered contract, not the requested one:
- When the response meets the requested tier (
met: true), you are charged for that tier. - A stale fallback is charged as the
cachedtier — you never pay a fresh-tier price for stale data. - Non-2xx responses carry no profile-data charge.
The actual charge is always reported in meta.billing.creditsCharged. Per-tier pricing is listed on the pricing page.
Response sections
The base sections — location, experience, accomplishments, publications, and patents — are always included. Extra sections are gated by two mutually exclusive parameters:
| Parameter | Description |
|---|---|
sections | Comma-separated extra sections to include: contactInformation, estimatedSalary, seniority, developerProfile, skills. Cannot be combined with exclude. |
exclude | Comma-separated extra sections to omit from the same list. Base sections cannot be excluded. Cannot be combined with sections. |
Individual enrichments are controlled by include/exclude flags. Each defaults to exclude:
| Parameter | Description |
|---|---|
extra | Include extra profile details. |
personal_email | Include personal emails. |
personal_contact_number | Include personal contact numbers. |
github_profile_id | Include the GitHub profile id, when resolvable. |
facebook_profile_id | Include the Facebook profile id, when resolvable. |
twitter_profile_id | Include the Twitter/X profile id, when resolvable. |
inferred_salary | Include the inferred salary range. |
skills | Include the skills list. |
github_data | Include the GitHub developer profile section. |
Handling errors
Errors are returned as RFC 9457 problem details (application/problem+json). Every error body carries a stable machine-readable code — branch on it, not on the HTTP status alone:
| Status | Code | Meaning |
|---|---|---|
400 | invalidInput | The request parameters are invalid — for example, no identifier was provided. |
400 | invalidFreshnessPolicy | The tier/policy combination is invalid — for example, allowStale=true with freshnessTier=live. |
404 | profileNotFound | No profile exists for the identifier. |
404 | profileInaccessible | The source reports the profile gone or not accessible as a profile. |
409 | cacheMiss | freshnessTier=cached and no cached profile exists. |
409 | cacheExpired | freshnessTier=cached and the cached profile exceeds the 90-day guarantee. |
500 | internalError | Something went wrong on our end. |
503 | profileFreshnessUnavailable | Strict freshness was requested and the source is unavailable. Retryable. |
504 | profileFreshnessTimeout | Strict freshness was requested and the source timed out. Retryable. |
A 404 is always a data-side outcome — the profile doesn't exist or can't be accessed at the source. Policy failures with freshnessTier=cached deliberately return 409, not 404: the profile may well exist, but the cache can't satisfy the guarantee you asked for. When that happens, retry with a tier that is allowed to fetch.
A 409 cacheExpired body looks like this:
{
"type": "https://enrichlayer.com/docs/problems/cache-expired",
"title": "Cached profile unavailable",
"status": 409,
"code": "cacheExpired",
"detail": "A cached profile exists, but it is older than the 90-day cached tier guarantee.",
"requestId": "8f14e45fceea167a5a36dedd4bea2543",
"requestedFreshness": { "tier": "cached" },
"lastKnownProfile": { "available": true, "profileAgeDays": 143 }
}503 and 504 responses set retryable: true — retrying the same request may succeed. If you would rather receive stale data than retry, set allowStale=true.
Migrating from v2
The v2 cache parameters do not map one-to-one onto v3 — in particular, there is no v3 equivalent of use_cache=if-present (return any cached result regardless of age). The closest tier, cached, never triggers a fetch and enforces a 90-day cap.
| v2 | v3 |
|---|---|
profile_url | profile |
live_fetch=force | freshnessTier=live |
fallback_to_cache=never | allowStale=false (the default) |
fallback_to_cache=on-error | allowStale=true |
use_cache=if-present | No direct equivalent — see above. |
max_age_days (earlier v3 draft) | freshnessTier=standard (the default), or one of the named tiers. |