요청 한도와 할당량
키 하나로 보낼 수 있는 요청 수와 개인정보 일일 한도.
Tahoe API에는 세 종류의 한도가 있습니다. 요청 한도는 키가 분당 보낼 수 있는 호출 수를 제한합니다. 일일 한도는 아무리 천천히 읽더라도 키가 하루에 읽을 수 있는 개인 데이터의 양을 제한합니다. 페이지 깊이 한도는 한 리스트를 10,000행보다 깊이 읽지 못하게 합니다. 모든 한도는 키별로 적용되며, GET /me에서 내 키의 값을 확인할 수 있습니다.
요청 한도
모든 엔드포인트는 세 등급 중 하나에 속합니다. 키마다 등급별로 분당 허용량이 따로 있습니다. 아래 숫자는 기본값입니다.
| 등급 | 기본 한도 | 엔드포인트 |
|---|---|---|
| sustained | 분당 600회 | 아래에 없는 모든 엔드포인트. 일반적인 리스트 읽기와 객체 읽기가 모두 여기에 속합니다. |
| expensive | 분당 60회 | POST /pool/search, POST /people/resolve:batch, GET /analytics/hiring-funnel, POST /jobs |
| download | 분당 30회 | GET /applications/{id}/resume/download, GET /sourced-profiles/{id}/attachments/{kind}/content |
한도를 넘으면 Retry-After 헤더와 함께 429 rate_limit_exceeded가 반환됩니다. 다시 시도하기 전에 그 초만큼 기다리세요. 한도는 IP 주소가 아니라 키별로 계산되므로, 연동을 더 많은 서버에서 실행해도 한도는 늘어나지 않습니다.
HTTP/1.1 429 Too Many Requests
Retry-After: 17
{
"detail": {
"code": "rate_limit_exceeded",
"type": "rate_limit",
"message": "Rate limit exceeded. Slow down and retry.",
"param": null,
"doc_url": "https://tahoe.workonward.com/developers/errors#rate_limit_exceeded",
"required_scope": null,
"retry_after_seconds": 17
}
}요금제별 한도
설정에서 만든 키는 그 키가 속한 워크스페이스의 요금제에 따라 한도가 정해집니다. 여유가 더 큰 요금제는 분당 허용량과 개인 데이터 일일 한도가 높고, 모든 요금제에는 워크스페이스의 모든 키가 함께 쓰는 월간 요청 할당량이 있습니다. 위의 수치는 Tahoe가 대신 준비해 드린 키의 기본값입니다.
GET /me는 호출에 쓴 키에 적용되는 한도를 rate_limits에 담아 반환합니다. 요금제, 분당 허용량, 월간 할당량, 워크스페이스가 지금까지 쓴 양이 들어 있습니다. 요금제가 없는 워크스페이스의 키는 가장 낮은 한도를 받으며, 월간 할당량을 모두 쓰면 429 rate_limit_exceeded가 반환되고 Retry-After에 남은 시간이 담깁니다.
개인 데이터 일일 한도
분당 한도와 별도로, 키마다 개인 데이터를 반환하는 읽기에 대한 일일 한도가 있습니다. 기본값은 하루 5,000개 값입니다. 요청 수가 아니라 반환된 값의 개수를 셉니다.
| 읽는 항목 | 스코프 | 차감량 |
|---|---|---|
| 이메일 주소 | contact:read | 반환된 주소 1개당 1 |
| 전화번호 | contact:phone:read | 반환된 번호 1개당 1 |
| 이력서 전체 텍스트 | resume:raw_text:read | 이력서 1개당 1 |
| 이력서 파일 또는 첨부 파일 | resume:download 또는 attachments:read | 파일 1개당 1 |
그 밖의 읽기는 이 한도를 쓰지 않습니다. 채용 공고, 지원서, 파이프라인은 하루 종일 페이지로 읽어도 이 한도에 영향이 없습니다. 이 한도는 일반적인 트래픽을 늦추려는 것이 아니라 개인 데이터의 대량 복사를 막기 위해 있습니다.
한도를 넘으면 429 personal_data_quota_exceeded가 반환됩니다. 한도는 UTC 자정에 초기화되며, Retry-After도 그때까지 남은 시간을 세므로 몇 초가 아니라 몇 시간이 될 수 있습니다. GET /me는 한도와 오늘 사용한 양을 모두 알려 주므로, 오래 걸리는 작업은 이 값을 지켜보다가 계획대로 멈출 수 있습니다.
{
"rate_limits": {
"general_per_minute": 600,
"expensive_per_minute": 60,
"download_per_minute": 30,
"personal_data_reads_per_day": 5000,
"personal_data_reads_used_today": 128,
"max_page_size": 100,
"default_page_size": 25,
"max_result_window": 10000
}
}페이지를 넘길 수 있는 깊이
한 리스트는 최대 10,000행 깊이까지 페이지를 넘길 수 있습니다. 그보다 깊이 가면 400 result_window_exceeded가 반환됩니다. 보통은 그렇게 깊이 갈 필요가 없습니다. 사본을 최신으로 유지하려면 다음 중 하나를 쓰세요.
- 마지막으로 성공한 동기화 시각부터
?updated_after=로 필터링합니다. - 이 용도로 만들어졌고 깊이 제한이 없는 변경 피드를 따라갑니다.
요청 한도 헤더
모든 응답에는 호출한 엔드포인트의 한도가 들어 있습니다.
HTTP/1.1 200 OK
Content-Type: application/json
Tahoe-Api-Version: 2026-09-09
Tahoe-Request-Id: req_3f9a1c7e5b2d4a60
RateLimit-Limit: 600
RateLimit-Policy: "sustained";q=600;w=60, "expensive";q=60;w=60, "download";q=30;w=60
X-RateLimit-Limit: 600| 헤더 | 의미 |
|---|---|
RateLimit-Limit | 방금 호출한 엔드포인트가 속한 등급의 분당 한도. |
RateLimit-Policy | 세 등급 전체와 각 등급의 한도(q), 시간 창(w, 초 단위). |
X-RateLimit-Limit | X- 헤더만 읽는 클라이언트를 위한 RateLimit-Limit 사본. |
Retry-After | 429에만 있습니다. 기다릴 초 수. |
좋은 클라이언트가 되려면
- 페이지를 꽉 채워 요청하세요.
limit=100이면 한 번으로 끝날 일을 기본값 25로는 네 번 요청해야 합니다. - 서버에서 필터링하세요. 모두 가져와서 대부분을 버리는 것보다
?status=published&updated_after=...가 낫습니다. - 변경 사항을 폴링하지 마세요. 변경 피드를 따라가세요. 수정 사항을 찾으려고 매분
/jobs를 읽으면 한도는 한도대로 쓰면서,job.updated이벤트 하나보다 알 수 있는 것은 적습니다. - 연락처는 필요할 때 읽으세요. 동기화 중에 모든 후보자의 전화번호를 가져오지 말고, 누군가 전화를 걸려고 할 때 가져오세요. 대부분의 연동이 가장 먼저 부딪히는 한도는 일일 한도이며, 거의 항상 아무도 보지 않은 값을 가져왔기 때문입니다.
- 키 하나에 작업자 하나를 두세요. 여러 작업자가 키 하나를 함께 쓰면 허용량도 함께 쓰게 되어, 429 응답이 무작위로 나오는 것처럼 보입니다.
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']}"
limits = SESSION.get(f"{BASE}/me", timeout=30).json()["rate_limits"]
left_today = limits["personal_data_reads_per_day"] - limits["personal_data_reads_used_today"]
# Leave headroom for anything else that uses this key today. Stopping on
# purpose is easy to resume; running into personal_data_quota_exceeded halfway
# through a backfill leaves you working out which records were written.
left = left_today - 200
for applicant_id in applicant_ids:
if left <= 0:
break # pick up the rest tomorrow, after midnight UTC
contact = SESSION.get(f"{BASE}/applicants/{applicant_id}/contact-info", timeout=30).json()
save_contact(applicant_id, contact)
# Every email and phone number returned uses one unit of the budget.
left -= len(contact.get("emails", [])) + len(contact.get("phones", []))