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.

ParameterConfidenceNotes
emailprobableAn individual work or personal address.
linkedin_urlexactA LinkedIn profile URL. The strongest identifier available to you.
coresignal_idexactA provider profile id, if you happen to hold one.

Send exactly one. Sending none or several is 400 invalid_request.

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"
Response
{
  "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"
  }
}
FieldMeaning
idThe person handle for the identity you resolved on.
canonical_idThe handle for the strongest identity known for this person. Store this, not id.
aliasesEvery other identity handle that leads to the same person.
authoritativeAlways false. A reminder that this resource is derived.
displayConvenience fields for showing a name in a UI.
display_sourceWhich profile the display block came from, so you can see which record to trust.
profilesPointers — object, handle, workspace, match quality and provenance. No values.
counts.exact_matchesHow 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).

Request
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]" }
    ]
  }'
Response
{
  "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.