API reference
People and identity
The same human can exist in Tahoe as an applicant, a sourced profile and a pool profile at once. A person is a derived view that points at all three — and it never merges their values.
The model, and the one thing to understand
A person is not a record. It is a derived index: an identity you resolved, plus pointers to the concrete profiles that identity matched. The payload says so in a field — "authoritative": false.
Nothing is merged. A person does not have “an email address”; it points at profiles which have their own. Authoritative reads go against those profiles.
GET/people/resolvepeople:resolve
The entry point. Give it one identifier you already hold and it tells you who that is in Tahoe.
| Parameter | Confidence | Notes |
|---|---|---|
email | probable | An individual work or personal address. |
linkedin_url | exact | A LinkedIn profile URL. The strongest identifier available to you. |
coresignal_id | exact | A provider profile id, if you happen to hold one. |
Send exactly one. Sending none or several is 400 invalid_request.
curl -s "https://tahoe.workonward.com/api/partner/v1/people/resolve?linkedin_url=https://www.linkedin.com/in/minjae-park-9x8y7z" \
-H "Authorization: Bearer $TAHOE_API_KEY"{
"object": "person",
"id": "per_bGlua2VkaW5fdXJsOm1pbmphZS1wYXJr",
"canonical_id": "per_bGlua2VkaW5fdXJsOm1pbmphZS1wYXJr",
"aliases": ["per_ZW1haWw6bWluamFlLnBhcmtAdG9zcy5pbQ"],
"identity": { "kind": "linkedin_url", "value": "minjae-park-9x8y7z" },
"authoritative": false,
"display": {
"full_name": "Minjae Park",
"job_title": "Staff Software Engineer",
"company_name": "Toss",
"location_full": "Seoul, South Korea"
},
"display_source": { "object": "sourced_profile", "id": "cnd_8Fj3kLm2Qd7s" },
"profiles": [
{
"object": "sourced_profile",
"id": "cnd_8Fj3kLm2Qd7s",
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"match": { "on": "linkedin_url", "confidence": "exact", "strength": "full" },
"provenance": { "origin": "sourced", "provider": "coresignal" }
},
{
"object": "applicant",
"id": "apl_2Xj7kQd4Rm8s",
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"match": { "on": "email", "confidence": "probable", "strength": "partial" },
"provenance": { "origin": "applicant", "provider": null }
}
],
"counts": {
"profiles": 2,
"sourced_profiles": 1,
"applicants": 1,
"pool_profiles": 0,
"exact_matches": 1
},
"links": {
"self": "/api/partner/v1/people/per_bGlua2VkaW5fdXJsOm1pbmphZS1wYXJr",
"profiles": "/api/partner/v1/people/per_.../profiles",
"contact_info": "/api/partner/v1/people/per_.../contact-info",
"resumes": "/api/partner/v1/people/per_.../resumes",
"applications": "/api/partner/v1/people/per_.../applications"
}
}| Field | Meaning |
|---|---|
id | The person handle for the identity you resolved on. |
canonical_id | The handle for the strongest identity known for this person. Store this, not id. |
aliases | Every other identity handle that leads to the same person. |
authoritative | Always false. A reminder that this resource is derived. |
display | Convenience fields for showing a name in a UI. |
display_source | Which profile the display block came from, so you can see which record to trust. |
profiles | Pointers — object, handle, workspace, match quality and provenance. No values. |
counts.exact_matches | How many pointers matched exactly. If this is 0, the whole resolution is a guess. |
A role address is refused with 400 not_an_identity. Keying a person on [email protected] would merge every recruiter at that company into one person, so it is rejected rather than resolved badly. A well-formed individual address that simply is not in Tahoe returns 404 instead.
POST/people/resolve:batchpeople:resolve
Up to 100 identities in one call. This is the endpoint for reconciling your own table against Tahoe — a hundred round trips would be both slower and noisier in your logs. In the expensive tier (60/minute).
curl -s -X POST https://tahoe.workonward.com/api/partner/v1/people/resolve:batch \
-H "Authorization: Bearer $TAHOE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keys": [
{ "kind": "linkedin_url", "value": "https://www.linkedin.com/in/minjae-park-9x8y7z" },
{ "kind": "email", "value": "[email protected]" },
{ "kind": "email", "value": "[email protected]" }
]
}'{
"object": "list",
"data": [
{
"object": "resolution",
"input": { "kind": "linkedin_url", "value": "https://www.linkedin.com/in/minjae-park-9x8y7z" },
"matched": true,
"person": { "object": "person", "id": "per_...", "canonical_id": "per_..." }
},
{
"object": "resolution",
"input": { "kind": "email", "value": "[email protected]" },
"matched": false,
"reason": "no_match"
},
{
"object": "resolution",
"input": { "kind": "email", "value": "[email protected]" },
"matched": false,
"reason": "not_an_identity"
}
],
"has_more": false,
"next_cursor": null
}Sent as a POST for the same reason as pool search: a hundred people’s email addresses in a query string end up in access logs, proxy caches and Referer headers. It is still an idempotent read, served no-store.
GET/people/{person_handle}people:resolve
Re-read a person from a handle you stored. Same envelope as /people/resolve.
GET/people/{person_handle}/profilespeople:resolve
Just the pointers. Use it when you have a person and want to know which concrete records to read authoritatively.
GET/people/{person_handle}/contact-infocontact:read
Contact information across every profile behind this person, each value attributed to the profile it came from. With contact:phone:read as well, phone numbers are included.
GET/people/{person_handle}/resumesresume:read
Every resume across the profiles behind this person, each with its own resume_access block.
GET/people/{person_handle}/applicationsapplications:read
Every application across the profiles behind this person — the fastest way to answer “has this candidate applied to us before?” from an identifier you hold rather than a handle you stored.
Related events
person.identity_linked and person.canonical_id_changed, both under people:resolve. The second is the one that matters: it means a handle you stored is no longer the canonical one for that human.
