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
testor alivekey (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.
GET /api/partner/v1/jobs HTTP/1.1
Host: tahoe.workonward.com
Authorization: Bearer thk_live_9tRc4mQx7Lb2sVfE1nKyP6wJdA3zHu8oT5iGq2Xw7Kd4mN3
Accept: application/jsoncurl -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:
- Deploy the new secret.
- Call
GET /mewith it to confirm it works. - 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):
{
"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:
{
"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.
| Scope | What it grants | Notes |
|---|---|---|
jobs:read | Job postings, their content, application forms and pipeline stages. | |
jobs:write | Create 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:manage | Publish, 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:read | Pre-screening questions and the job's 6-digit phone code. | |
applications:read | Applications, their stage and status, and match scores. | |
applications:write | Create 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:write | Add 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:write | Add interview scorecards to an application. | Changes data. Paid plan. API keys only, unless writes for connected apps are switched on. |
messages:send | Send 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:read | Answers applicants typed into your application form. | Personal data |
applications:internal:read | Rejection reasons, AI score rationale and stage history. | Personal data: internal comments 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. Free: it spends no credits. | Personal data |
contact:read | Work and personal email addresses your workspace has already revealed. | Personal data. Every value is recorded and counts toward the daily budget. |
contact:phone:read | Phone numbers your workspace has already revealed. | Personal data. Every value is recorded and counts toward the daily budget. |
resume:read | Resume metadata and the parsed profile. | Personal data |
resume:raw_text:read | The full text of a resume. | Personal data. Every value is recorded and counts toward the daily budget. |
resume:download | Download resume and attachment files. | Personal data. Every value is recorded and counts toward the daily budget. |
attachments:read | Profile photos and the list of attachments. | Personal data. Each file download is recorded and counts toward the daily budget. |
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. | |
lists:write | Create 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:read | Aggregate hiring-funnel numbers. | |
events:read | The change feed and the erasure notices. | |
webhooks:read | Your webhook endpoint settings and delivery log. | |
webhooks:write | Register, 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.
| Group | Scopes | Who can grant it |
|---|---|---|
| Included | jobs:read, applications:read, applicants:read, users:read, workspaces:read, lists:read, analytics:read, events:read, webhooks:read | Any workspace owner or admin. |
| Paid plan | contact: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:write | An 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 available | pool:read, pool:search, people:resolve, sourced_profiles:read, attachments:read, resume:raw_text:read | Not 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:readis separate fromcontact:readbecause a phone number is the most sensitive contact detail Tahoe holds.resume:raw_text:readis separate fromresume:readbecause the full text is the whole document. Guarding the file but not its text would guard nothing.applications:internal:readis separate fromapplications:readbecause 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:writeletsPOST /jobscreate 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:manageis separate fromjobs:writebecause 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:writeandscorecards:writeare 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:sendsends email to people who applied, with Tahoe’s unsubscribe link and a daily cap per key. See Messages.lists:writecreates lists and fills them. See Create and fill lists.webhooks:writeregisters 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=:
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.
{
"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.