Skip to content

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.

ParameterTypeNotes
afterintegerReturn events after this sequence. Leave it out to start from the oldest event you can see.
typestringOne or more event types, comma-separated. An unknown name is refused with 400 unknown_event_type, not ignored.
workspace_idhandleOnly events from this workspace. Useful for a key that reaches several workspaces.
limitintegerDefault 25, maximum 100.
Request
curl "https://tahoe.workonward.com/api/partner/v1/events?after=48210&limit=100" \
  -H "Authorization: Bearer $TAHOE_API_KEY"
Response
{
  "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

FieldMeaning
idThe event handle (evt_). Use it to skip an event you have already handled.
typeOne of the 37 types listed below.
api_versionThe API version the event was shaped by, fixed when it was written.
created_atWhen the event was written.
sequenceYour resume point. Always increasing, never reused, not contiguous.
workspace_idThe workspace that changed, or null for an event that belongs to no single workspace.
data.objectThe 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.changedOn some events: the names of the fields that changed.
data.object.originOn 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_attributesOn some events: the old values of simple fields such as status or stage_id.
links.selfThe 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.

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

GroupEventSent whenScope
Jobsjob.publishedA posting went live.jobs:read
job.updatedA posting changed, and no more specific job event applies.jobs:read
job.closedA posting closed.jobs:read
job.reopenedA closed posting went live again.jobs:read
job.unpublishedA posting was taken back to draft.jobs:read
job.deletedA posting was deleted. Carries a reason.jobs:read
job.sections_updatedThe content sections of a posting changed.jobs:read
Applicationsapplication.createdSomeone applied.applications:read
application.updatedAn application changed.applications:read
application.status_changedThe status of an application changed.applications:read
application.stage_changedAn application moved to another pipeline stage.applications:read
application.withdrawnThe applicant withdrew.applications:read
application.deletedAn application was deleted. Carries a reason.applications:read
application.scoredA match score was calculated.applications:read
application.resume_parsedThe resume was read and the parsed profile is ready.resume:read
application.screening_completedA phone pre-screen finished. Says only that it happened and how it went.screening:metadata:read
Resumesresume.access_changedA resume moved between open, view-only and locked.resume:read
Applicantsapplicant.createdA new applicant record was created.applicants:read
applicant.updatedAn applicant record changed.applicants:read
applicant.deletedAn applicant record was deleted. Carries a reason.applicants:read
applicant.unsubscribedThe person asked not to be contacted.applicants:read
Sourced profilessourced_profile.createdThe team saved a new candidate.sourced_profiles:read
sourced_profile.updatedA saved candidate changed.sourced_profiles:read
sourced_profile.deletedA saved candidate was deleted. Carries a reason.sourced_profiles:read
sourced_profile.contact_info_revealedNew contact details were revealed. Names the fields, never the values.contact:read
sourced_profile.attachment_addedAn attachment was added to the profile.attachments:read
Listslist.createdA list was created.lists:read
list.updatedA list changed.lists:read
list.deletedA list was deleted.lists:read
list_membership.addedA candidate was added to a list.lists:read
list_membership.removedA candidate was removed from a list.lists:read
list_membership.stage_changedA candidate moved to another stage within a list.lists:read
Shared poolpool.batch_upsertedA batch of shared-pool profiles was added or updated. One event per batch, not per profile.pool:read
Peopleperson.identity_linkedA profile was linked to an existing person, for example an applicant whose email matches a saved candidate.people:resolve
person.canonical_id_changedThe canonical ID of a person changed. Remap the handle you stored.people:resolve
Data subjectdata_subject.suppression_appliedTahoe stopped processing a person. Delete your copy within 7 days.events:read
data_subject.erasure_completedTahoe destroyed the record of a person.events:read

Filtering by type

Only job events
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 id you 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_after filter on the list endpoints lets you check.
A change-feed reader
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