Blog
Why profile and company lookups return 404 when the identifier looks correct
A display name, a urn:li: prefix, or the wrong url field 404s an enrichment lookup, and every 404 still costs a credit. Fix the four identifier traps first.
A 404 on an enrichment lookup is not a miss you can shrug off. It costs one credit, the same
credit a 200 costs, because searching for an identifier that turns out to have no record is the
same retrieval as finding one. So a run that 404s on a bad identifier is a run you paid for and
got nothing back from. Four input mistakes cause almost all of these, and all four are fixable
before you spend a credit.
Both endpoints key off a slug, not a name
The Person API and the Company API each take one
required identifier: profile_id on GET /v1/people/profile, and company_id on
GET /v1/companies/details. Despite the name, company_id takes the URL slug from
/company/<slug>, not the display name. Send Acme Corporation and you get a 404, and that
404 is billed like any answered lookup.
| You send as the identifier | What resolves | Outcome |
|---|---|---|
Acme Corporation | nothing, it is not a slug | 404, 1 credit |
acme | the company slug | 200, 1 credit |
urn:li:member:214703388 | rejected, prefixed urn | 400, free |
ACoAAB1exampleUrn9Xy | the bare entity urn | 200, 1 credit |
Trap 1: a display name where a slug belongs
Company display names never resolve. company_id wants the slug that appears in the address bar
on a /company/<slug>/ page, which is not the name printed on the page and is often not even
close to it. Resolve the slug from a real company URL, never from the name. If all you have is a
name, that is a search problem, and there is no search endpoint here: enriching by name means
finding the slug first, then spending one credit on the lookup.
Trap 2: the urn:li: prefix on an entity urn
For people, the identifier worth storing is entity_urn, returned as a bare ACoAAB1exampleUrn9Xy
string. Feed that straight back in as profile_id and it resolves. Feed back the
urn:li:member:214703388 form and you get a 400 Bad Request for a malformed identifier: the
prefixed member urn is an output field, not an input. When you
map a person record onto your own schema, store the ACo… value, not
the public_identifier slug, which the profile owner can edit and eventually will.
Trap 3: company_url is not platform_url
A company record carries two URL fields whose names are the reverse of what intuition suggests.
platform_url is the page on the source platform, https://www.linkedin.com/company/northwind-labs.
company_url is the company’s own website, https://www.northwindlabs.com. Feed company_url
back in as an identifier and you 404 on a domain that was never a slug. The slug you can re-request
with lives in platform_url. This mix-up, and the rest of the company field traps, is covered in
how to store and refresh company records.
Trap 4: specialities, not specialties
This one does not 404, which is worse. The call succeeds, you read the American spelling
specialties, get back nothing, and ship an empty array to production. The field is spelled the
British way:
// The record came back 200. This still returns undefined.
const wrong = company.specialties; // undefined, wrong spelling
const right = company.specialities; // ["Data infrastructure", ...]
Provenance lives in the response headers, never in the body, so a wrong key name fails silently against the JSON and there is no error to catch. Spell it the way the payload does.
Resolve before you spend
Put one function between your data and the request, so a display name or a stray URL can never reach the wire as an identifier. Node:
// A full profile URL and a bare slug both work; a display name does not.
function profileId(input) {
const m = input.match(/linkedin\.com\/in\/([^/?#]+)/i);
return (m ? m[1] : input).trim();
}
const res = await fetch(
`https://api.triguna.ai/v1/people/profile?profile_id=${encodeURIComponent(profileId(who))}`,
{ headers: { ApiKey: process.env.TRIGUNA_KEY } },
);
if (res.status === 404) {
markMissing(who); // Billed once. Do not retry: the identifier resolved to no record.
}
Python, for the company side:
import os, re, requests
def company_id(value: str) -> str:
# Pull the slug out of a /company/<slug>/ URL; never pass a display name.
m = re.search(r"/company/([^/?#]+)", value)
return (m.group(1) if m else value).strip()
r = requests.get(
"https://api.triguna.ai/v1/companies/details",
params={"company_id": company_id(value)},
headers={"ApiKey": os.environ["TRIGUNA_KEY"]},
)
The whole point of the surface being two endpoints and one auth header is that this guard is small enough to hold in your head. The full accepted forms are in the identifiers reference.
Next
- Map a person record to your own schema, and store the entity urn.
- Store and refresh company records, including the url field mix-up.
- Why a cache hit still costs a credit, the same billing rule as a 404.
- Enrich a user on signup without letting a bad identifier block the request.
- Start with five free credits at app.triguna.ai/signup, no card.