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.

Request
GET /api/partner/v1/jobs HTTP/1.1
Host: tahoe.workonward.com
Authorization: Bearer thk_live_9tRc4mQx7Lb2sVfE1nKyP6wJdA3zHu8oT5i
Accept: application/json

Secrets 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:

401 Unauthorized
{
  "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.

ScopeGrantsNotes
jobs:readJob postings, their content, application forms and pipeline stages.
jobs:screening:readPre-screening questions and the job's 6-digit phone code.
applications:readApplications, their stage and status, and match scores.
applications:answers:readAnswers applicants typed into your application form.Personal data
applications:internal:readRejection reasons, AI score rationale and stage history.Personal data — candid internal commentary about a named person
screening:metadata:readWhether a phone screening happened and how it went. Never its content.Personal data
applicants:readPeople who applied to your jobs.
sourced_profiles:readCandidates your workspace has sourced and saved.
people:resolveMatch your own records to Tahoe people by email or LinkedIn URL.
pool:readTahoe's shared pool of public professional profiles.Personal data
pool:searchSearch the shared pool. Costs nothing and calls no provider.Personal data
contact:readWork and personal email addresses your workspace has already revealed.Personal data · audited per record · metered
contact:phone:readPhone numbers your workspace has already revealed.Personal data · audited per record · metered
resume:readResume metadata and the parsed structured profile.Personal data
resume:raw_text:readThe full text of a resume document.Personal data · audited per record · metered
resume:downloadDownload resume and attachment files.Personal data · audited per record · metered
attachments:readProfile photos and the attachment index.Personal data
users:readThe people in your workspace and their roles.
workspaces:readWorkspace name and creation date.
lists:readProjects, lists, and where each candidate sits in your pipeline.
analytics:readAggregate hiring-funnel numbers.
events:readThe change feed and the erasure feed.
webhooks:readYour webhook endpoint configuration and delivery log.

Why some scopes are split

Three splits exist because gating the parent would have gated nothing:

  • contact:phone:read is separate from contact:read because a phone number is the most valuable single field in the product.
  • resume:raw_text:read is separate from resume:read because the raw text is the whole document. Gating the file download and not the text would gate nothing.
  • applications:internal:read is separate from applications:read because 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:

Cross-workspace request
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.