Getting started
Quickstart
Mint a key, confirm what it can do, and page your first list. About five minutes, and no SDK to install.
1. Create a key
In Tahoe, open Settings → Developer and press Create key. The dialog asks for four things:
- Name. This appears in the key list and on every line of the audit log. Name it after the system that will hold the key, not the person creating it — “WorkOnward HRIS sync”, not “Priya’s key”.
- Environment.
liveortest. Both read the same data through the same URL; the prefix on the secret is what lets a log reader and a secret scanner tell them apart. - Expiry. Days until the key stops working. Rotate before then. Zero means it never expires, which you should treat as a last resort.
- Scopes. Grant the narrowest set that does the job. You can rotate the secret without changing scopes, but changing scopes means a new key.
2. Confirm the credential
Before writing any integration code, ask the API what your key can do. This turns “my calls 403” from a debugging session into a one-line answer.
export TAHOE_API_KEY="thk_live_..."
curl -s https://tahoe.workonward.com/api/partner/v1/me \
-H "Authorization: Bearer $TAHOE_API_KEY" | jqRead three fields from the response. scopes is what you were granted. workspace_ids is what you can reach. workspace_id_required tells you whether every subsequent call must name a workspace explicitly — it is true only for cross-workspace credentials.
3. Read a list
Lists are cursored, not offset-paged. Ask for a page, then follow next_cursor until it comes back null.
curl -s "https://tahoe.workonward.com/api/partner/v1/jobs?limit=25&status=published" \
-H "Authorization: Bearer $TAHOE_API_KEY" | jq{
"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,
"accept_applications": true,
"summary": "Own the ingestion pipeline behind Tahoe's candidate graph.",
"published_at": "2026-08-14T09:02:11.004Z",
"created_at": "2026-08-12T15:41:07.882Z",
"updated_at": "2026-09-08T11:20:45.331Z",
"links": { "self": "/api/partner/v1/jobs/job_7Kd2mXq4Rp8v" }
}
],
"has_more": true,
"next_cursor": "cur_eyJrIjoiMjAyNi0wOC0xNFQwOTowMjoxMVoiLCJpIjoiam9iXzdLZDJt..."
}4. Page all the way through
import os
import requests
BASE = "https://tahoe.workonward.com/api/partner/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['TAHOE_API_KEY']}"
def page_all(path, **params):
"""Yield every row from a cursored list endpoint."""
cursor = None
while True:
query = {**params}
if cursor:
# Send ONLY the cursor and the params it was minted with. A cursor is
# signed against its query, so changing a filter mid-loop is rejected
# with invalid_cursor rather than silently returning the wrong rows.
query["cursor"] = cursor
response = SESSION.get(f"{BASE}{path}", params=query, timeout=30)
response.raise_for_status()
body = response.json()
yield from body["data"]
cursor = body.get("next_cursor")
if not cursor:
return
for job in page_all("/jobs", status="published", limit=100):
print(job["id"], job["title"])5. Stay current without re-reading everything
Once you have a copy, do not re-read the whole workspace on a schedule. Read the change feed from where you left off:
curl -s "https://tahoe.workonward.com/api/partner/v1/events?limit=100&type=job.published,job.updated,job.closed" \
-H "Authorization: Bearer $TAHOE_API_KEY" | jqPersist the next_after the response gives you and resume with ?after=<sequence>. That sequence number is your sync state — a timestamp is not, because events are written by concurrent transactions and can appear slightly out of wall-clock order.
What to read next
- Authentication — scopes, IP allowlists, rotation, and cross-workspace credentials.
- Conventions — handles, pagination, timestamps, and the
restrictedcontract you must handle to avoid caching a lie. - Errors — the full catalogue, and which codes are worth retrying.
- Agent prompt — if an LLM is writing the integration, give it this.
