Quickstart
Make your first API call in a few minutes.
This page takes you from no key to a complete read of your published jobs, and then shows how to keep your copy current without reading everything again. You need curl for the first steps and Python for the paging example. There is no SDK to install: the API is plain HTTPS and JSON.
Create a key
In Tahoe, open Settings, choose Developer, then Create key. You need to be an owner or admin of the workspace. Choose:
- the scopes it needs (pick the narrowest set that does the job);
- whether it is a
testor alivekey, and a name for it, such as “Acme HRIS sync”. Name the key after the system that will hold it, not after a person; - how long it lasts, and optionally the IP addresses or ranges it may be used from.
The key reads the workspace you created it in. If Settings has no Developer tab, email [email protected].
Check what the key can do
Before you write any integration code, ask the API about the key itself. GET /me turns “why do my calls fail?” 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"{
"object": "credential",
"id": "pk_9tRc4mQx7Lb2",
"name": "Acme HRIS sync",
"environment": "live",
"scopes": ["applications:read", "events:read", "jobs:read"],
"workspace_scope": "list",
"workspace_ids": ["wsp_4Kd8sPm2Qx7L"],
"workspace_id_required": false,
"expires_at": "2026-12-08T10:14:22.510Z",
"api_version": "2026-09-09"
}Read three fields:
scopes: what the key may read.workspace_ids: the workspaces it can reach.workspace_id_required: whether every call must name a workspace with?workspace_id=. Most keys belong to one workspace, and this isfalse.
Read a page of jobs
Lists come back one page at a time. Ask for a page, then follow next_cursor until it is null.
curl -s "https://tahoe.workonward.com/api/partner/v1/jobs?status=published&limit=25" \
-H "Authorization: Bearer $TAHOE_API_KEY"{
"object": "list",
"data": [
{
"object": "job",
"id": "job_7Kd2mXq4Rp8v",
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"slug": "field-coordinator-a41f",
"status": "published",
"title": "Field Coordinator",
"department": "Operations",
"employment_type": "full_time",
"location_type": "onsite",
"locations": ["Columbus, Ohio"],
"experience_level": "mid",
"years_min": 2,
"years_max": 5,
"accept_applications": true,
"summary": "Run day-to-day site schedules for our Ohio crews.",
"source": {
"system": "tahoe_native",
"external_id": null,
"company_name": null,
"company_logo_url": null,
"canonical_url": null
},
"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..."
}Page all the way through
The loop below reads every published job. It works for any list endpoint: change the path and the filters.
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, **filters):
"""Yield every row from a list endpoint, following next_cursor."""
cursor = None
while True:
# Send the same filters on every page, plus the cursor. A cursor is
# bound to the filters it came from, so changing one mid-loop is
# refused with invalid_cursor.
params = dict(filters)
if cursor:
params["cursor"] = cursor
response = SESSION.get(f"{BASE}{path}", params=params, 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"])Send the same filters with every page. You may change limit between pages, but a changed filter makes the cursor invalid (400 invalid_cursor). Cursors also expire after an hour, so finish a pass rather than pausing halfway.
Keep up with changes
Once you have a full copy, do not read the whole workspace again on a schedule. Read the change feed instead. It lists every change in order, oldest first.
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"Each event has a sequence number. Save the sequence of the last event you processed, and next time start after it:
curl -s "https://tahoe.workonward.com/api/partner/v1/events?after=48213&limit=100&type=job.published,job.updated,job.closed" \
-H "Authorization: Bearer $TAHOE_API_KEY"The sequence number is your sync position. A timestamp is not: changes are written at the same time by different processes, so their times can arrive slightly out of order. The key needs the events:read scope for this, plus the scope of each resource you want to hear about.