API reference
Jobs
Seven endpoints. The list is thin by design and the single read is fat: mirror a board from the list, then fetch sections only for the jobs you actually render.
GET/jobsjobs:read
Jobs in the workspace, newest first. Returns published and closed jobs by default — a job board that mirrors this naively will never accidentally publish a customer’s unfinished draft.
| Parameter | Type | Notes |
|---|---|---|
status | string | Comma-separated. Defaults to published,closed. Anything else needs include_unpublished=true. |
include_unpublished | boolean | Opt in to drafts, archived and other non-public states. Without it, asking for them is 400 unpublished_requires_opt_in. |
department | string | Exact match. |
location_type | string | e.g. remote, hybrid, onsite. |
employment_type | string | e.g. full_time, contract. |
q | string | Full-text search over the job. |
updated_after | RFC 3339 | For incremental sync. This is the parameter to reach for instead of deep paging. |
published_after | RFC 3339 | Only jobs published since. |
limit | integer | Default 25, max 100. |
cursor | string | From the previous page. Send the identical filters alongside it. |
curl -s "https://tahoe.workonward.com/api/partner/v1/jobs?status=published&limit=100" \
-H "Authorization: Bearer $TAHOE_API_KEY"{
"object": "list",
"data": [
{
"object": "job",
"id": "job_7Kd2mXq4Rp8v",
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"slug": "senior-backend-engineer-a41f",
"status": "published",
"title": "Senior Backend Engineer",
"department": "Engineering",
"team": "Platform",
"employment_type": "full_time",
"location_type": "hybrid",
"locations": ["Seoul, KR"],
"experience_level": "senior",
"years_min": 5,
"years_max": 9,
"compensation": {
"salary_min": 90000000,
"salary_max": 130000000,
"currency": "KRW",
"interval": "year",
"unit": "major",
"equity": null,
"commission": null
},
"accept_applications": true,
"external_apply_url": null,
"source": {
"system": "tahoe_native",
"company_name": null,
"company_logo_url": null,
"canonical_url": null
},
"summary": "Own the ingestion pipeline behind Tahoe's candidate graph.",
"published_at": "2026-08-14T09:02:11.004Z",
"close_at": null,
"created_at": "2026-08-12T15:41:07.882Z",
"updated_at": "2026-09-08T11:20:45.331Z",
"application_count": 148,
"links": {
"self": "/api/partner/v1/jobs/job_7Kd2mXq4Rp8v",
"sections": "/api/partner/v1/jobs/job_7Kd2mXq4Rp8v/sections"
}
}
],
"has_more": true,
"next_cursor": "cur_eyJrIjoiMjAyNi0wOC0xNFQwOTowMjoxMVoi..."
}The list is thin, the single read is fat
A list row carries summary. The full content object — description markdown, responsibilities, requirements, skills, benefits — appears only on a single-job read. Paging 100 full descriptions to render a board that shows titles would be pure waste for both sides.
GET/jobs/{job_handle}jobs:read
One job, with content instead of summary:
{
"object": "job",
"id": "job_7Kd2mXq4Rp8v",
"content": {
"summary": "Own the ingestion pipeline behind Tahoe's candidate graph.",
"description_md": "## About the role\n\nYou will own ...",
"responsibilities": ["Design and run the ingestion pipeline", "..."],
"requirements": ["5+ years in Python", "..."],
"nice_to_have": ["Experience with pgvector"],
"skills_required": ["Python", "PostgreSQL", "asyncio"],
"skills_preferred": ["Kubernetes"],
"benefits": ["Private health cover", "..."]
},
"restricted": ["voice_screening.dial_in_code"],
"restricted_reason": {
"voice_screening.dial_in_code": "scope_required:jobs:screening:read"
}
}description_md is Markdown. Render it as Markdown or strip it — do not inject it into a page as HTML without sanitising, because it is customer-authored text.
GET/jobs/by-slug/{slug}jobs:read
Resolve a public job slug to a job. Use this when you hold a URL a candidate clicked — from a job board, an email, or a referral link — and need the handle behind it, rather than storing a slug as a foreign key. Slugs are public and can change; handles cannot.
GET/jobs/{job_handle}/sectionsjobs:read
The full structured job in one call — the same content object as the single read, for callers that want the body without re-reading the metadata they already have.
GET/jobs/{job_handle}/pipeline-stagesjobs:read
The stages this job’s pipeline is made of, in order. You need these to make sense of an application’s stage_id — read them once per job and cache them, then resolve a stage handle locally.
{
"object": "list",
"data": [
{
"object": "pipeline_stage",
"id": "stg_3Rp8vKd2mXq4",
"job_id": "job_7Kd2mXq4Rp8v",
"name": "Phone screen",
"position": 2,
"type": "screening"
}
],
"has_more": false,
"next_cursor": null
}Stages are per job, not per workspace: two jobs can both have a stage called “Phone screen” with different handles. Key your local copy on the stage handle, not the name.
GET/jobs/{job_handle}/application-formjobs:read
The schema of the form applicants filled in. This is what makes form answers interpretable: an answer carries a field id, and this is where the label, type and options for that id live.
{
"object": "application_form",
"job_id": "job_7Kd2mXq4Rp8v",
"is_template": false,
"fields": [
{
"id": "why_this_role",
"label": "Why are you interested in this role?",
"type": "long_text",
"required": true,
"section": "questions",
"help_text": "A few sentences is plenty.",
"options": [],
"max_length": 2000
},
{
"id": "work_authorisation",
"label": "Are you authorised to work in South Korea?",
"type": "single_select",
"required": true,
"section": "eligibility",
"help_text": null,
"options": ["Yes", "No", "Require sponsorship"],
"max_length": null
}
]
}Retired fields are filtered out, so a form you read today describes the form as it stands. Answers from an older submission can therefore reference a field id that is no longer listed — keep the label you saw at the time rather than resolving ids lazily.
GET/jobs/{job_handle}/prescreen-questionsjobs:screening:read
The questions a phone screening asks, grouped into common (seeded from a template and asked on every posting) and adaptive (generated from this job’s own description, capped at five).
{
"object": "prescreen_questions",
"job_id": "job_7Kd2mXq4Rp8v",
"common": [
{
"id": "f19c...",
"position": 1,
"prompt": "Tell me about your experience with distributed systems.",
"response_key": "distributed_systems",
"answer_type": "free_text",
"choices": null,
"required": true,
"source": "template"
}
],
"adaptive": [
{
"id": "8b2a...",
"position": 1,
"prompt": "Have you worked with pgvector or another vector index in production?",
"response_key": "vector_index_experience",
"answer_type": "free_text",
"choices": null,
"required": false,
"source": "generated"
}
]
}Related events
Rather than re-reading /jobs on a timer, subscribe to these on the change feed: job.published, job.updated, job.closed, job.reopened, job.unpublished, job.deleted and job.sections_updated. All seven need only jobs:read.
