API reference
Applications
Ten endpoints. An application is the join between a job and a person, and almost everything an ATS integration needs hangs off it.
GET/applicationsapplications:read
Applications across the workspace, newest first.
| Parameter | Type | Notes |
|---|---|---|
job_id | handle | Restrict to one job. Equivalent to the nested route below. |
status | string | Comma-separated. |
stage_id | handle | Applications currently sitting in one stage. |
updated_after | RFC 3339 | For incremental sync. |
applied_after | RFC 3339 | Only applications submitted since. |
limit | integer | Default 25, max 100. |
cursor | string | From the previous page. |
{
"object": "list",
"data": [
{
"object": "application",
"id": "app_6Qm2xKd4Rp8v",
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"job_id": "job_7Kd2mXq4Rp8v",
"applicant_id": "apl_5Nx3jLm7Qd2s",
"stage_id": "stg_3Rp8vKd2mXq4",
"status": "in_review",
"source": "careers_site",
"applied_at": "2026-09-02T14:21:09.115Z",
"updated_at": "2026-09-07T09:44:31.002Z",
"parse_status": "parsed",
"has_resume": true,
"voice_screening_opted_out": false,
"links": {
"self": "/api/partner/v1/applications/app_6Qm2xKd4Rp8v",
"answers": "/api/partner/v1/applications/app_6Qm2xKd4Rp8v/answers",
"resume": "/api/partner/v1/applications/app_6Qm2xKd4Rp8v/resume"
},
"restricted": ["rejection_reason"],
"restricted_reason": {
"rejection_reason": "scope_required:applications:internal:read"
}
}
],
"has_more": true,
"next_cursor": "cur_eyJrIjoiMjAyNi0wOS0wMlQxNDoyMTowOVoi..."
}voice_screening_opted_out is not decoration. If it is true, the candidate declined phone screening, and any workflow of yours that would call them must respect that.
GET/jobs/{job_handle}/applicationsapplications:read
The same rows, scoped to one job. Prefer this to fetching everything and filtering client-side — it is one request instead of many pages, and it keeps you well away from the paging window.
GET/applications/{application_handle}applications:read
One application. Same shape as a list row. With applications:internal:read, rejection_reason is present instead of restricted.
GET/applications/{application_handle}/answersapplications:answers:read
What the applicant typed into your customer’s application form, keyed by field id. Resolve the ids against the job’s application form to get labels and types.
{
"object": "application_answers",
"application_id": "app_6Qm2xKd4Rp8v",
"answers": {
"why_this_role": "I have spent four years on ingestion pipelines and ...",
"work_authorisation": "Yes"
},
"restricted": ["answers"],
"restricted_reason": {
"answers": "filtered:not_in_current_form — retired, consent, or equal-opportunity fields are never returned"
}
}GET/applications/{application_handle}/scoreapplications:read
Tahoe’s match assessment for this application against this job.
{
"object": "application_score",
"application_id": "app_6Qm2xKd4Rp8v",
"match_pct": 82,
"gaps": ["No Kubernetes experience stated"],
"restricted": ["rationale", "model_version"],
"restricted_reason": {
"rationale": "scope_required:applications:internal:read",
"model_version": "scope_required:applications:internal:read"
}
}rationale is the model’s written reasoning about a named person, which is why it sits behind applications:internal:read rather than travelling with the number. If you surface match_pct to a recruiter, label it as Tahoe’s assessment rather than presenting it as your own.
GET/applications/{application_handle}/screeningscreening:metadata:read
Whether a phone screening happened and how it went. Metadata only — and unlike most withheld fields, no scope exists that returns the content.
{
"object": "screening",
"application_id": "app_6Qm2xKd4Rp8v",
"has_screening": true,
"call_status": "completed",
"completion_state": "all_questions_answered",
"duration_sec": 412,
"consent_state": "granted",
"ambiguous_caller": false,
"started_at": "2026-09-04T11:02:18.440Z",
"ended_at": "2026-09-04T11:09:10.771Z",
"restricted": ["responses", "transcript", "audio"],
"restricted_reason": {
"responses": "never_exposed:consent_scope",
"transcript": "never_exposed:consent_scope",
"audio": "never_exposed:consent_scope"
}
}When no screening has happened the response is {"has_screening": false} rather than a 404, so a nightly sync can treat “no call yet” as data instead of an error.
GET/applications/{application_handle}/stage-transitionsapplications:internal:read
Where this application has moved and when. Enough to reconstruct time-in-stage and build a funnel of your own.
{
"object": "list",
"data": [
{
"object": "stage_transition",
"from_stage_id": null,
"to_stage_id": "stg_9Kd2mXq4Rp8v",
"at": "2026-09-02T14:21:09.115Z"
},
{
"object": "stage_transition",
"from_stage_id": "stg_9Kd2mXq4Rp8v",
"to_stage_id": "stg_3Rp8vKd2mXq4",
"at": "2026-09-05T16:38:52.309Z"
}
],
"has_more": false,
"next_cursor": null
}No actor and no free text. Which recruiter moved a candidate, and what they wrote while doing it, are not part of this response. The timeline is the fact; the commentary is not.
GET/applications/{application_handle}/resumeresume:read
Resume metadata, the parsed structured profile, and the access state that says whether you may read or download it.
{
"object": "resume",
"id": "res_2Kd8sPm4Qx7L",
"application_id": "app_6Qm2xKd4Rp8v",
"filename": "priya-raman-cv.pdf",
"mime": "application/pdf",
"bytes": 184320,
"parse_status": "parsed",
"resume_access": {
"state": "view_only",
"free_until": "2026-09-09T14:21:09.115Z",
"view_until": "2026-10-02T14:21:09.115Z",
"state_changes_at": "2026-10-02T14:21:09.115Z",
"unlock_credits": 5,
"can_view": true,
"can_download": false
},
"current_title": "Senior Data Engineer",
"current_company": "Kakao",
"seniority": "senior",
"total_years_experience": 7,
"city": "Seoul",
"country": "KR",
"remote_ok": true,
"education_level": "bachelors",
"skills": ["Python", "Airflow", "PostgreSQL"],
"languages": ["Korean", "English"],
"extraction_confidence": 0.94,
"parsed_at": "2026-09-02T14:23:41.088Z",
"structured": { "experience": [], "education": [] },
"links": {
"download": "/api/partner/v1/applications/app_6Qm2xKd4Rp8v/resume/download"
}
}GET/applications/{application_handle}/resume/downloadresume:download
A short-lived signed URL for the resume file. In the download rate-limit tier (30/minute), and every call consumes one unit of the daily personal-data budget and writes an audit row.
curl -s https://tahoe.workonward.com/api/partner/v1/applications/app_6Qm2xKd4Rp8v/resume/download \
-H "Authorization: Bearer $TAHOE_API_KEY"{
"object": "resume_download",
"application_id": "app_6Qm2xKd4Rp8v",
"url": "https://tahoe.sfo3.digitaloceanspaces.com/...&X-Amz-Signature=...",
"expires_in": 300,
"filename": "priya-raman-cv.pdf",
"mime": "application/pdf",
"bytes": 184320
}Fetch the URL within expires_in seconds and do not store it — it is a bearer capability for that one file. Store the application handle and request a fresh URL when you next need the bytes.
If the workspace has not purchased access, this returns 403 resume_locked with state and unlock_credits so you can tell a user what unlocking costs. The API mirrors the product’s paywall and is never a cheaper path to a document.
Related events
application.created, application.updated, application.status_changed, application.stage_changed, application.withdrawn, application.deleted and application.scored need applications:read. application.resume_parsed and resume.access_changed need resume:read; application.screening_completed needs screening:metadata:read.
