Skip to content

API keys and scopes

Create, rotate and revoke API keys, and choose exactly what each key can read and change.

Every request to the Tahoe API carries an API key. The key decides three things: which workspace the request reads, which kinds of data it may see (its scopes), and where it may be used from. This page covers how to get a key, how to send it, what each of the 31 scopes grants, and how to rotate or revoke a key.

Getting a key

Workspace owners and admins create keys themselves. Open Settings, choose Developer, then Create key. You choose:

  • a name for the key, such as “Acme HRIS sync”;
  • whether it is a test or a live key (or make one of each);
  • the scopes it needs, from the table below;
  • how long it lasts, from 1 to 365 days;
  • optionally, the IP addresses or ranges it may be used from.

You also accept the API terms. The secret is shown once, so copy it into your secret manager before you close the dialog. A key belongs to the workspace it was created in and cannot read any other.

Ask for the narrowest set of scopes that does the job. Scopes are fixed when the key is created: adding one later means a new key.

Sending the key

Send the key as a bearer token in the Authorization header on every request. No other form is accepted: not a query parameter, not a cookie, not basic auth.

Request
GET /api/partner/v1/jobs HTTP/1.1
Host: tahoe.workonward.com
Authorization: Bearer thk_live_9tRc4mQx7Lb2sVfE1nKyP6wJdA3zHu8oT5iGq2Xw7Kd4mN3
Accept: application/json
The same call with curl
curl -s https://tahoe.workonward.com/api/partner/v1/jobs \
  -H "Authorization: Bearer $TAHOE_API_KEY"

Key format

A key looks like thk_live_ or thk_test_, followed by 43 random characters and a 4-character checksum. The fixed thk_ prefix lets secret scanners spot a key that was committed by mistake. The checksum means a key that was cut short or mistyped is refused straight away.

Tahoe keeps only a one-way hash of the secret. Nobody at Tahoe can read a key back to you, so store it in your secret manager as soon as you receive it.

Test and live keys

Test and live keys read the same workspace through the same base URL. There is no separate sandbox with sample data, so a test key still reads real records. The prefix is a label: it makes a test key easy to spot in your logs, and lets you revoke all of one kind without touching the other.

Expiry

Every key you create expires, and you pick the term when you create it: from 1 to 365 days. The default is 90 days. GET /me shows the date in expires_at. After that date the key gets the same 401 as an unknown key, so plan a rotation before then.

IP allowlist

A key can carry a list of IP addresses or CIDR ranges, such as 203.0.113.0/24. A request from any other address is refused with the standard 401. Use the allowlist as an extra layer of protection on top of keeping the key secret, not instead of it.

Rotating a key

To replace a secret, for example on a schedule or before a key expires, choose Rotate on the key in Settings, under Developer. You get a new secret with the same name, scopes, workspace and allowlist. The old secret keeps working for 7 days by default, so you can switch over without downtime:

  1. Deploy the new secret.
  2. Call GET /me with it to confirm it works.
  3. Let the old secret lapse at the end of the overlap.

Revoking a key

Revoking a key stops it at once and for good. There is no way to undo it. Choose Revoke on the key in Settings, under Developer, whenever a secret may have leaked. After that, calls with it get the same 401 as a key that never existed. Webhook endpoints that belong to the key stop delivering at the same moment.

One 401 for every key problem

A missing key, an unknown key, a revoked or expired key, a malformed key and a request from outside the allowlist all return 401 with the code unauthenticated. The response never says which of these it was (a missing header only gets a reminder to send the key):

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
  }
}

This is deliberate. If the API told “revoked” apart from “never existed”, anyone holding a stolen key could use it to test whether the key was still good. If you cannot tell why a key fails, check expires_at from an earlier /me call and the key list in Settings, then contact us.

Scopes

A scope is permission to read one kind of data. A key carries only the scopes it was created with. Calling an endpoint without its scope returns 403 insufficient_scope, and required_scope names the scope you need:

403 Forbidden
{
  "detail": {
    "code": "insufficient_scope",
    "type": "permission",
    "message": "This credential does not carry the contact:read scope.",
    "param": null,
    "doc_url": "https://tahoe.workonward.com/developers/errors#insufficient_scope",
    "required_scope": "contact:read",
    "retry_after_seconds": null
  }
}

A missing scope on a single field is not an error. The request succeeds, and the field is listed in restricted instead. See withheld fields.

ScopeWhat it grantsNotes
jobs:readJob postings, their content, application forms and pipeline stages.
jobs:writeCreate and update job postings pushed in from your own system, and publish them to the Tahoe job board. Never deletes, and never touches a posting created in Tahoe.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.
jobs:managePublish, unpublish, close and reopen any job in the workspace, including jobs a recruiter created in Tahoe. Never deletes.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.
jobs:screening:readPre-screening questions and the job's 6-digit phone code.
applications:readApplications, their stage and status, and match scores.
applications:writeCreate applications, move them between stages and reject them. Moves are recorded in the application’s history and visible to your team.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.
notes:writeAdd notes to an application. Notes are visible to everyone who can see the application.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.
scorecards:writeAdd interview scorecards to an application.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.
messages:sendSend single emails to candidates from your workspace. Replies go to the person who created the key, and unsubscribed people are never sent to.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.
applications:answers:readAnswers applicants typed into your application form.Personal data
applications:internal:readRejection reasons, AI score rationale and stage history.Personal data: internal comments 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. Free: it spends no credits.Personal data
contact:readWork and personal email addresses your workspace has already revealed.Personal data. Every value is recorded and counts toward the daily budget.
contact:phone:readPhone numbers your workspace has already revealed.Personal data. Every value is recorded and counts toward the daily budget.
resume:readResume metadata and the parsed profile.Personal data
resume:raw_text:readThe full text of a resume.Personal data. Every value is recorded and counts toward the daily budget.
resume:downloadDownload resume and attachment files.Personal data. Every value is recorded and counts toward the daily budget.
attachments:readProfile photos and the list of attachments.Personal data. Each file download is recorded and counts toward the daily budget.
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.
lists:writeCreate lists, add candidates to them and move candidates between list stages.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.
analytics:readAggregate hiring-funnel numbers.
events:readThe change feed and the erasure notices.
webhooks:readYour webhook endpoint settings and delivery log.
webhooks:writeRegister, change, test and disable your own webhook endpoints.Changes data. Paid plan. API keys only, unless writes for connected apps are switched on.

The daily budget for personal data is explained on Rate limits and quotas.

Which scopes you can give a key

Scopes come in three groups. The Create key dialog shows each scope with its group and locks the ones you cannot grant.

GroupScopesWho can grant it
Includedjobs:read, applications:read, applicants:read, users:read, workspaces:read, lists:read, analytics:read, events:read, webhooks:readAny workspace owner or admin.
Paid plancontact:read, contact:phone:read, resume:read, resume:download, applications:answers:read, applications:internal:read, jobs:screening:read, screening:metadata:read, and every scope that writes: jobs:write, jobs:manage, applications:write, notes:write, scorecards:write, messages:send, lists:write, webhooks:writeAn owner or admin of a workspace with an active subscription, who also confirms that they will handle the personal data lawfully. A scope that writes also needs writes to be switched on.
Not availablepool:read, pool:search, people:resolve, sourced_profiles:read, attachments:read, resume:raw_text:readNot offered for keys you create. They cover data that is not your own or that Tahoe may not pass on.

Plans also set how fast a key may call the API and how many requests the workspace may make in a month. Settings shows the numbers for your plan, and GET /me returns them with your use so far. See Rate limits and quotas.

Why some scopes are split

  • contact:phone:read is separate from contact:read because a phone number is the most sensitive contact detail Tahoe holds.
  • resume:raw_text:read is separate from resume:read because the full text is the whole document. Guarding the file but not its text would guard nothing.
  • applications:internal:read is separate from applications:read because rejection reasons and score rationale are frank comments about a named person, written by someone who did not expect the candidate to read them.

Scopes that write

Eight scopes change data instead of reading it: jobs:write, jobs:manage, applications:write, notes:write, scorecards:write, messages:send, lists:write and webhooks:write. Every one of them is a paid plan scope. They appear in the Create key dialog when writes are switched on, and a key that holds them can use only the endpoints that name them.

  • jobs:write lets POST /jobs create or update a posting from your own system and publish it. It never edits a posting a recruiter created in Tahoe. See the Jobs reference.
  • jobs:manage is separate from jobs:write because it reaches every job in the workspace. A key that may push its own postings should not also be able to close a job a recruiter wrote. See Publish, unpublish, close and reopen.
  • applications:write, notes:write and scorecards:write are three scopes so that a key that only annotates cannot also move or reject a candidate. See Change an application and Create an application.
  • messages:send sends email to people who applied, with Tahoe’s unsubscribe link and a daily cap per key. See Messages.
  • lists:write creates lists and fills them. See Create and fill lists.
  • webhooks:write registers your own webhook endpoints. It is offered only where registering endpoints through the API is switched on. See Manage your endpoints.

No write deletes anything. Every write endpoint is a POST, and the API has no PUT, PATCH or DELETE.

Who a write is attributed to

A write is attributed to the person who created the key. Notes, scorecards, stage moves, rejections and sent messages appear in Tahoe under that person’s name, as if they had done it by hand, and the key’s activity is limited by what that person could do. A key that cannot be tied to a Tahoe user gets 403 write_requires_api_key and cannot write at all.

Recruiters can tell that an integration did it. The activity of an application, job or list gets an extra entry whose kind starts with partner_api_, such as partner_api_stage_move or partner_api_note_added. It holds the key ID and the request ID, never the text of a note or a message. Use one key per integration, and name it clearly, so that the person who reads the activity can find the integration behind an entry.

Which workspace a request reads

Most keys belong to exactly one workspace, and you never mention it: the key already knows. Such a key cannot be pointed at another workspace. Naming a workspace it does not cover returns 404, not 403, because even confirming that a workspace exists would tell you something you should not know.

A key that can reach more than one workspace must name its target on every call with ?workspace_id=:

Naming the workspace
curl -s "https://tahoe.workonward.com/api/partner/v1/jobs?workspace_id=wsp_4Kd8sPm2Qx7L" \
  -H "Authorization: Bearer $TAHOE_API_KEY"

Leaving it out returns 400 workspace_id_required. There is no default and no “all workspaces”, so one forgotten parameter cannot read a workspace you did not mean to.

400 Bad Request
{
  "detail": {
    "code": "workspace_id_required",
    "type": "invalid_request",
    "message": "This credential can reach multiple workspaces, so workspace_id is required.",
    "param": "workspace_id",
    "doc_url": "https://tahoe.workonward.com/developers/errors#workspace_id_required",
    "required_scope": null,
    "retry_after_seconds": null
  }
}

To find out which kind of key you hold, call GET /me: check workspace_ids and workspace_id_required.

Tokens from Sign in with Tahoe

An access token from Sign in with Tahoe is sent the same way, as a bearer token to the same base URL. It acts as one Tahoe user. What it can read is the overlap of the scopes your app asked for and what that user can see in Tahoe. It can write only when writes for connected apps are switched on, and then as that user. Every personal-data read made with it is recorded against that user as well as your app.