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. live or test. 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.

Request
export TAHOE_API_KEY="thk_live_..."

curl -s https://tahoe.workonward.com/api/partner/v1/me \
  -H "Authorization: Bearer $TAHOE_API_KEY" | jq

Read 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.

Request
curl -s "https://tahoe.workonward.com/api/partner/v1/jobs?limit=25&status=published" \
  -H "Authorization: Bearer $TAHOE_API_KEY" | jq
Response
{
  "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

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

Request
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" | jq

Persist 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 restricted contract 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.