후보자 미러링
지원자와 소싱한 후보자를 내 시스템으로 복사하세요.
이 가이드는 Tahoe 워크스페이스의 사람들을 내 시스템으로 복사하는 방법을 설명합니다. 고객의 채용 공고에 지원한 지원자와, 고객의 팀이 찾아 저장한 후보자가 대상입니다. 모든 연동 중 지켜야 할 의무가 가장 많으므로, 필요 이상으로 수집하지 않는 방법과 가려진 필드를 빈 필드로 착각하지 않는 방법도 함께 다룹니다.
두 종류의 사람
| 지원자 | 소싱한 프로필 | |
|---|---|---|
| 엔드포인트 | /applicants | /sourced-profiles |
| 스코프 | applicants:read | sourced_profiles:read |
| 들어온 경로 | 채용 공고에 지원했거나, 고객의 ATS에서 가져왔습니다 | 팀의 누군가가 찾아 저장했습니다 |
provenance.origin | applicant 또는 ats_import | sourced |
| 동의 근거 | candidate_submitted (ATS에서 가져온 경우 customer_provided) | legitimate_interest_sourcing |
| Tahoe의 고지문 표시 | 예, Tahoe를 통해 지원한 경우 | 아니요 |
| 연락처 | 본인이 제출한 정보입니다. 크레딧 없이 볼 수 있습니다. | 워크스페이스가 이미 열람한 것만 제공됩니다. 읽어도 크레딧이 들지 않습니다. |
두 그룹은 별도 테이블로 두거나, 최소한 모든 행에 provenance.origin을 함께 저장하세요. 하나의 “후보자” 테이블로 합치면 각 사람에게 무엇을 해도 되는지 결정하는 차이가 사라집니다. 누군가 질문한다면 가장 먼저 물어볼 부분이 바로 이것입니다.
사본 만들기
구조부터 복사하기
import os
import requests
BASE = "https://tahoe.workonward.com/api/partner/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TAHOE_API_KEY']}"
def backfill_applicants(store):
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
response = session.get(f"{BASE}/applicants", params=params, timeout=30)
response.raise_for_status()
body = response.json()
for applicant in body["data"]:
provenance = applicant["provenance"]
store.upsert_person(
tahoe_id=applicant["id"],
# "applicant" or "ats_import" here; "sourced" on /sourced-profiles.
origin=provenance["origin"],
full_name=applicant["full_name"],
location=applicant["location"],
linkedin_url=applicant["linkedin_url"],
# Kept so a compliance question can be answered without
# another API call.
consent_basis=provenance["consent"]["basis"],
unsubscribed=provenance["consent"]["unsubscribed"],
external_refs=applicant["external_refs"],
# No contact details here. They are fetched when needed.
contact_state="unknown",
)
cursor = body["next_cursor"]
if not cursor:
return이 루프의 어떤 부분도 개인 데이터 한도를 쓰지 않습니다. 이제 누가 있는지, 각자 어디서 왔는지, 내 시스템에서 어떻게 찾는지 알 수 있습니다. /sourced-profiles도 같은 방법으로 넘기며 읽으세요. 시작하기 전에 증분 동기화에서 설명한 대로 변경 피드 워터마크를 기록하세요.
내 레코드와 사람 연결하기
ATS에서 온 지원자라면 external_refs에 그 시스템의 ID가 있습니다. system과 id를 함께 맞추면 끝입니다. 이름을 비교하거나 추측할 필요가 없습니다.
그 밖의 경우에는 LinkedIn URL이나 이메일로 한 번에 최대 100명까지 확인(resolve)하세요.
def reconcile(store):
"""Match up to 100 of our own records to Tahoe people in one call."""
batch = [
{"kind": "linkedin_url", "value": row.linkedin_url}
if row.linkedin_url
else {"kind": "email", "value": row.email}
for row in store.unmatched(limit=100)
]
response = session.post(f"{BASE}/people/resolve:batch", json={"keys": batch}, timeout=30)
response.raise_for_status()
body = response.json()
# Results come back IN ORDER, unmatched ones included, so pairing them
# with the input by position is safe and nothing goes missing.
for sent, result in zip(batch, body["data"]):
if not result["matched"]:
store.mark_unresolved(sent, reason=result["reason"])
continue
person = result["person"]
if person["counts"]["exact_matches"] == 0:
# Matched on email alone: probable, not certain. Ask a human.
store.queue_for_review(sent, person["canonical_id"])
continue
# canonical_id, not id: the same human found by email and by LinkedIn
# URL has two ids but one canonical_id.
store.link_person(sent, person["canonical_id"])연락처는 필요할 때 가져오기
연동이 하루 개인 데이터 한도 안에 머무는 데 이 결정 하나가 가장 큰 역할을 합니다.
def contact_for(store, applicant_id):
"""Fetch contact details at the moment a recruiter opens the record."""
cached = store.contact(applicant_id)
if cached and cached.fresh:
return cached
response = session.get(f"{BASE}/applicants/{applicant_id}/contact-info", timeout=30)
response.raise_for_status()
body = response.json()
# "restricted" means Tahoe holds the value and this key may not read it.
# Storing an empty value would record "this person has no phone", and
# nothing would make you look again once the scope is granted.
withheld = set(body.get("restricted") or [])
store.save_contact(
applicant_id,
emails=None if "emails" in withheld else body["emails"],
phones=None if "phones" in withheld else body["phones"],
withheld=sorted(withheld),
field_states=body["field_states"],
# Never gated by scopes. If true, do not contact this person.
unsubscribed=body["unsubscribed"],
)
return store.contact(applicant_id)| 보이는 것 | 저장할 값 | 이렇게 저장하면 안 됨 |
|---|---|---|
emails나 phones에 값이 있음 | 그 값 | |
field_states가 not_found | Tahoe에 없음 | |
restricted에 이름이 있음 | 알 수 없음: 가려짐 | 빈 값 또는 null |
이력서 접근 권한은 매번 확인하기
지원서와 함께 들어온 이력서는 지원 후 90일 동안 무료로 보고 내려받을 수 있고, 120일째까지는 보기만 가능합니다. 그 뒤에는 워크스페이스의 누군가가 Tahoe에서 Unlock resume을 눌러 잠금을 풀 때까지 잠깁니다(50크레딧, 한 번만, 영구 해제). API로는 이력서 잠금을 풀 수 없습니다.
따라서 내 쪽에서 아무 일이 없어도 접근 권한이 바뀔 수 있습니다. resume_access(state, can_view, can_download)를 캐시하지 말고 매번 읽고, resume.access_changed를 받아 처리하세요. 그러지 않으면 화면에 내려받기 버튼이 보이는데 누르면 403 resume_locked로 실패합니다.
다운로드 링크는 5분 뒤 만료되며 파일 하나에만 쓸 수 있습니다. 이력서의 핸들을 저장해 두고 파일이 필요할 때 새 링크를 요청하세요. 링크 자체는 저장하지 마세요.
최신 상태 유지하기
HANDLERS = {
"applicant.created": reread_applicant,
"applicant.updated": reread_applicant,
"applicant.deleted": purge_applicant,
# Wire this one first. It is what stops you emailing someone who asked
# not to be contacted.
"applicant.unsubscribed": mark_unsubscribed,
"sourced_profile.created": reread_sourced,
"sourced_profile.updated": reread_sourced,
"sourced_profile.deleted": purge_sourced,
# Names the fields that were revealed, never the values. Drop your
# cached contact details and fetch them again when they are needed.
"sourced_profile.contact_info_revealed": invalidate_contact_cache,
"resume.access_changed": reread_resume_access,
# A handle you stored is no longer the canonical one for this person.
"person.canonical_id_changed": remap_person,
# An instruction, not information. See the deletion guide.
"data_subject.suppression_applied": on_data_subject_event,
"data_subject.erasure_completed": on_data_subject_event,
}지켜야 할 약속
licence.redistributable은 항상 false입니다. 이 데이터는 고객의 채용 업무 안에서만 쓰세요. 이 데이터로 인물 디렉터리를 만들거나, 재판매하거나, 다른 사람들이 검색하는 제품에 넣지 마세요.- 수신 거부와 삭제(소거) 요청을 지키세요. 삭제와 소거를 참고하세요.
- 대량으로 내보내지 마세요. 페이지 넘김은 목록당 10,000행에서 멈춥니다(
400 result_window_exceeded). 증분 동기화가 지원되는 방법이며 비용도 적게 듭니다. - 지원자와 소싱한 프로필은 따로 보관하세요. 내 저장소에 보관하는 동안 계속 그렇게 하세요.