Getting started
Authentication
One bearer token, 23 scopes, and an explicit answer to 'which workspace does this request mean'.
The header
Send the secret as a bearer token on every request. There is no other accepted form: no query parameter, no cookie, no basic auth.
GET /api/partner/v1/jobs HTTP/1.1
Host: tahoe.workonward.com
Authorization: Bearer thk_live_9tRc4mQx7Lb2sVfE1nKyP6wJdA3zHu8oT5i
Accept: application/jsonSecrets look like thk_live_<43 characters><4 checksum> or thk_test_…. The fixed thk_ prefix is there so a secret scanner can recognise one in a commit, and the trailing checksum means a truncated or mistyped key is rejected as malformed instead of producing a puzzling 401 you spend an afternoon on.
Every authentication failure is the same 401
Missing, unknown, revoked, expired, malformed, or refused by the IP allowlist — all of them return one identical body:
{
"detail": {
"code": "unauthenticated",
"type": "authentication",
"message": "Invalid or expired credential.",
"param": null,
"doc_url": "https://tahoe.workonward.com/developers/errors#unauthenticated",
"required_scope": null,
"retry_after_seconds": null,
"request_id": "req_8Kf2mQx4Rp"
}
}This is deliberate. Distinguishing “revoked” from “never existed” would turn the endpoint into an oracle for validating stolen tokens. If you need to know which it is, look at the key in Settings → Developer, where the status is shown plainly.
Scopes
A scope is granted when the key is created and cannot be widened afterwards — changing scopes means minting a new key. Calling an endpoint without its scope returns 403 insufficient_scope, and the response names the scope you needed in required_scope, so the fix never requires guesswork.
| Scope | Grants | Notes |
|---|---|---|
jobs:read | Job postings, their content, application forms and pipeline stages. | |
jobs:screening:read | Pre-screening questions and the job's 6-digit phone code. | |
applications:read | Applications, their stage and status, and match scores. | |
applications:answers:read | Answers applicants typed into your application form. | Personal data |
applications:internal:read | Rejection reasons, AI score rationale and stage history. | Personal data — candid internal commentary about a named person |
screening:metadata:read | Whether a phone screening happened and how it went. Never its content. | Personal data |
applicants:read | People who applied to your jobs. | |
sourced_profiles:read | Candidates your workspace has sourced and saved. | |
people:resolve | Match your own records to Tahoe people by email or LinkedIn URL. | |
pool:read | Tahoe's shared pool of public professional profiles. | Personal data |
pool:search | Search the shared pool. Costs nothing and calls no provider. | Personal data |
contact:read | Work and personal email addresses your workspace has already revealed. | Personal data · audited per record · metered |
contact:phone:read | Phone numbers your workspace has already revealed. | Personal data · audited per record · metered |
resume:read | Resume metadata and the parsed structured profile. | Personal data |
resume:raw_text:read | The full text of a resume document. | Personal data · audited per record · metered |
resume:download | Download resume and attachment files. | Personal data · audited per record · metered |
attachments:read | Profile photos and the attachment index. | Personal data |
users:read | The people in your workspace and their roles. | |
workspaces:read | Workspace name and creation date. | |
lists:read | Projects, lists, and where each candidate sits in your pipeline. | |
analytics:read | Aggregate hiring-funnel numbers. | |
events:read | The change feed and the erasure feed. | |
webhooks:read | Your webhook endpoint configuration and delivery log. |
Why some scopes are split
Three splits exist because gating the parent would have gated nothing:
contact:phone:readis separate fromcontact:readbecause a phone number is the most valuable single field in the product.resume:raw_text:readis separate fromresume:readbecause the raw text is the whole document. Gating the file download and not the text would gate nothing.applications:internal:readis separate fromapplications:readbecause rejection reasons and score rationale are candid internal commentary about a named person, written by someone who did not expect the candidate to read it.
Which workspace a request means
Most keys are pinned to exactly one workspace, and you never mention it — the credential already knows. Nothing you can send will make such a key read a different workspace: naming someone else’s workspace returns 404, not 403, because confirming that a workspace exists but is not yours is itself a leak.
A cross-workspace credential is different, and it must name its target on every single call:
curl -s "https://tahoe.workonward.com/api/partner/v1/jobs?workspace_id=wsp_4Kd8sPm2Qx7L" \
-H "Authorization: Bearer $TAHOE_API_KEY"Omitting it is 400 workspace_id_required. There is no default and no implicit “all”, because one accidentally unscoped query is how a multi-tenant sync sweeps a whole database. Check workspace_id_required on /me to find out which kind of key you are holding.
Environments
live and test keys read the same workspace through the same base URL. Tahoe does not maintain a separate sandbox dataset, so a test key is not a safety net against reading real personal data — it is a label that makes an accident visible in a log and lets you revoke one class of credential without touching the other.
IP allowlists
A key can carry a list of addresses or CIDR ranges, and a request from anywhere else is refused with the standard 401. Treat this as defence in depth rather than a boundary: requests reach Tahoe through a proxy chain, so the address the API sees is only as trustworthy as that chain.
Rotation and revocation
Rotate issues a new secret for the same key — same id, same name, same scopes — and keeps the previous secret working for a short overlap. Deploy the new one, verify, and let the old one lapse. This is the path for a routine credential refresh.
Revoke kills a key immediately and permanently. Use it when a secret may have leaked. There is no un-revoke; a revoked key’s calls return the same 401 as a key that never existed.
Tokens from Sign in with Tahoe
An access token minted by the OIDC flow authenticates the same way — bearer, same base URL — but it acts as a named human, and its reach is the intersection of what your app asked for and what that person can actually see. Audit rows for those requests record the acting user, because a disclosure made under a delegated credential was authorised by a person, and an audit that said only “app X read this” would lose that. See Sign in with Tahoe.
