Change feed
Read every change in order and keep your copy up to date.
The change feed is one ordered log of everything that changes in the workspaces your key can read: jobs, applications, applicants, saved candidates, lists, the shared pool, people and erasure notices. Read it from where you left off and you never need to re-read a whole workspace on a timer.
Webhooks push the same events to your server as they happen. The feed is still the record you recover from, so most integrations read it even when they also take webhooks.
List events
GET/eventsevents:read
Returns one page of events, oldest first. That is the opposite of other lists in this API, because a feed you replay has to be read forwards.
| Parameter | Type | Notes |
|---|---|---|
after | integer | Return events after this sequence. Leave it out to start from the oldest event you can see. |
type | string | One or more event types, comma-separated. An unknown name is refused with 400 unknown_event_type, not ignored. |
workspace_id | handle | Only events from this workspace. Useful for a key that reaches several workspaces. |
limit | integer | Default 25, maximum 100. |
curl "https://tahoe.workonward.com/api/partner/v1/events?after=48210&limit=100" \
-H "Authorization: Bearer $TAHOE_API_KEY"{
"object": "list",
"data": [
{
"object": "event",
"id": "evt_7Kd2mXq4Rp8v",
"type": "job.published",
"api_version": "2026-09-09",
"created_at": "2026-09-08T11:20:45.331Z",
"sequence": 48214,
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"data": {
"object": {
"object": "job",
"id": "job_3Hn6tWq9Lc2v",
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"updated_at": "2026-09-08T11:20:45.112Z",
"status": "published"
},
"previous_attributes": { "status": "draft" }
},
"links": { "self": "/api/partner/v1/events/evt_7Kd2mXq4Rp8v" }
},
{
"object": "event",
"id": "evt_2Wp5nRc8Kd3x",
"type": "application.stage_changed",
"api_version": "2026-09-09",
"created_at": "2026-09-08T11:24:02.907Z",
"sequence": 48219,
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"data": {
"object": {
"object": "application",
"id": "app_6Qm2xKd4Rp8v",
"workspace_id": "wsp_4Kd8sPm2Qx7L",
"updated_at": "2026-09-08T11:24:02.880Z",
"stage_id": "stg_3Rp8vKd2mXq4",
"status": "in_review",
"changed": ["stage_id"]
},
"previous_attributes": { "stage_id": "stg_9Kd2mXq4Rp8v" }
},
"links": { "self": "/api/partner/v1/events/evt_2Wp5nRc8Kd3x" }
}
],
"has_more": true,
"next_cursor": null,
"next_after": 48219
}The event object
| Field | Meaning |
|---|---|
id | The event handle (evt_). Use it to skip an event you have already handled. |
type | One of the 37 types listed below. |
api_version | The API version the event was shaped by, fixed when it was written. |
created_at | When the event was written. |
sequence | Your resume point. Always increasing, never reused, not contiguous. |
workspace_id | The workspace that changed, or null for an event that belongs to no single workspace. |
data.object | The thing that changed: its object type, id, workspace_id and updated_at, plus a few plain values such as status, stage_id or reason. |
data.object.changed | On some events: the names of the fields that changed. |
data.object.origin | On an event caused by a write through the API: api: and the ID of the key that wrote. Absent for changes made in Tahoe. See The origin of an event. |
data.previous_attributes | On some events: the old values of simple fields such as status or stage_id. |
links.self | The path of this event. Join it onto https://tahoe.workonward.com. |
Get one event
GET/events/{event_handle}events:read
One event, by handle, in the same shape as a row of the feed. Use it to look again at a webhook delivery you logged, or to replay a handle from a dead-letter queue.
curl https://tahoe.workonward.com/api/partner/v1/events/evt_7Kd2mXq4Rp8v \
-H "Authorization: Bearer $TAHOE_API_KEY"Event types
There are 37 event types. Reading an event needs events:read plus the scope that lets you read the resource it describes. Events your key could not read are left out of the feed, so the feed never tells you about records your scopes hide.
| Group | Event | Sent when | Scope |
|---|---|---|---|
| Jobs | job.published | A posting went live. | jobs:read |
job.updated | A posting changed, and no more specific job event applies. | jobs:read | |
job.closed | A posting closed. | jobs:read | |
job.reopened | A closed posting went live again. | jobs:read | |
job.unpublished | A posting was taken back to draft. | jobs:read | |
job.deleted | A posting was deleted. Carries a reason. | jobs:read | |
job.sections_updated | The content sections of a posting changed. | jobs:read | |
| Applications | application.created | Someone applied. | applications:read |
application.updated | An application changed. | applications:read | |
application.status_changed | The status of an application changed. | applications:read | |
application.stage_changed | An application moved to another pipeline stage. | applications:read | |
application.withdrawn | The applicant withdrew. | applications:read | |
application.deleted | An application was deleted. Carries a reason. | applications:read | |
application.scored | A match score was calculated. | applications:read | |
application.resume_parsed | The resume was read and the parsed profile is ready. | resume:read | |
application.screening_completed | A phone pre-screen finished. Says only that it happened and how it went. | screening:metadata:read | |
| Resumes | resume.access_changed | A resume moved between open, view-only and locked. | resume:read |
| Applicants | applicant.created | A new applicant record was created. | applicants:read |
applicant.updated | An applicant record changed. | applicants:read | |
applicant.deleted | An applicant record was deleted. Carries a reason. | applicants:read | |
applicant.unsubscribed | The person asked not to be contacted. | applicants:read | |
| Sourced profiles | sourced_profile.created | The team saved a new candidate. | sourced_profiles:read |
sourced_profile.updated | A saved candidate changed. | sourced_profiles:read | |
sourced_profile.deleted | A saved candidate was deleted. Carries a reason. | sourced_profiles:read | |
sourced_profile.contact_info_revealed | New contact details were revealed. Names the fields, never the values. | contact:read | |
sourced_profile.attachment_added | An attachment was added to the profile. | attachments:read | |
| Lists | list.created | A list was created. | lists:read |
list.updated | A list changed. | lists:read | |
list.deleted | A list was deleted. | lists:read | |
list_membership.added | A candidate was added to a list. | lists:read | |
list_membership.removed | A candidate was removed from a list. | lists:read | |
list_membership.stage_changed | A candidate moved to another stage within a list. | lists:read | |
| Shared pool | pool.batch_upserted | A batch of shared-pool profiles was added or updated. One event per batch, not per profile. | pool:read |
| People | person.identity_linked | A profile was linked to an existing person, for example an applicant whose email matches a saved candidate. | people:resolve |
person.canonical_id_changed | The canonical ID of a person changed. Remap the handle you stored. | people:resolve | |
| Data subject | data_subject.suppression_applied | Tahoe stopped processing a person. Delete your copy within 7 days. | events:read |
data_subject.erasure_completed | Tahoe destroyed the record of a person. | events:read |
Filtering by type
curl "https://tahoe.workonward.com/api/partner/v1/events?after=48210&type=job.published,job.closed" \
-H "Authorization: Bearer $TAHOE_API_KEY"New event types are added without a new API version. Ignore a type you do not recognize instead of failing on it. Use ?type= only when you really want just those types: a filter written today leaves out the types added tomorrow.
Delivery and retention
- At least once. You may see the same event more than once. Skip any
idyou have already handled, and write handlers that are safe to run twice. A handler that adds to a counter or appends a row will drift. - Kept for 30 days. If your reader falls more than 30 days behind, the oldest events are gone. Start again with a full copy, as described in Incremental sync.
- Recorded after the change. Tahoe records an event after the change it describes, and it never holds up a change in the product to record one. If you suspect you missed something, the
updated_afterfilter on the list endpoints lets you check.
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 drain(store):
"""Apply every new event, oldest first. Safe to run as often as you like."""
after = store.load_sequence() # None on the very first run
while True:
params = {"limit": 100}
if after is not None:
params["after"] = after
response = session.get(f"{BASE}/events", params=params, timeout=30)
response.raise_for_status()
body = response.json()
for event in body["data"]:
# Delivery is at-least-once: the same event can arrive twice.
if not store.already_processed(event["id"]):
handle(event)
store.mark_processed(event["id"])
# Save the watermark only AFTER the event is handled, so a crash
# replays the event instead of skipping it. A gap in sequence
# numbers is normal and is never a reason to re-read.
after = event["sequence"]
store.save_sequence(after)
if not body["has_more"]:
return