Staying in sync

Erasure notices

The one feed in this API that is an instruction rather than information. If you store anything you read from Tahoe, this is the endpoint you are obliged to consume.

GET/erasuresevents:read

Every notice that applies to you, oldest first, keyset-paginated.

ParameterTypeNotes
limitintegerDefault 25, max 100.
cursorstringFrom the previous page. Unlike the change feed, this one does use a cursor.
Request
curl -s "https://tahoe.workonward.com/api/partner/v1/erasures?limit=100" \
  -H "Authorization: Bearer $TAHOE_API_KEY"
Response
{
  "object": "list",
  "data": [
    {
      "object": "erasure",
      "id": "ers_5Nx3jLm7Qd2s",
      "action": "suppression_applied",
      "reason": "data_subject_erasure",
      "action_required": "cease_processing_and_delete_your_copy",
      "workspace_id": "wsp_4Kd8sPm2Qx7L",
      "at": "2026-09-07T14:02:11.408Z",
      "deadline_at": "2026-09-14T14:02:11.408Z",
      "destruction_pending": true,
      "destruction_reason": "retention_floor",
      "resources": [
        {
          "object": "sourced_profile",
          "id": "cnd_8Fj3kLm2Qd7s",
          "workspace_id": "wsp_4Kd8sPm2Qx7L"
        }
      ]
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Two actions, and the difference is your whole obligation

actionMeansWhat you must do
suppression_appliedTahoe has stopped processing this person.Act now. Delete your own copy by deadline_at and stop processing them.
erasure_completedTahoe has now destroyed the record.Confirm your own deletion is done. This is the closing entry, not the instruction.

deadline_at is seven days after the notice. That is the window you have, not a suggestion.

Why there are two events and not one

Tahoe’s own retention floor can lawfully refuse destruction for up to four years — employment records have statutory retention in several jurisdictions. So “stop processing” and “it is destroyed” are genuinely different moments, sometimes years apart.

Your obligation attaches to the suppression, not the erasure. When destruction_pending is true, Tahoe is still holding the record under a retention floor it cannot ignore, with destruction_reason saying which. That has no bearing on your deadline: you delete your copy within seven days regardless.

Reasons

reasonOrigin
data_subject_erasureThe person asked. The strongest form.
suppressionThey asked not to be processed, short of full erasure.
provider_takedownA data provider or platform required removal.
user_deletedA recruiter deleted the record.
workspace_deletedThe whole workspace was deleted.
retention_expiryA retention period ran out.
legal_hold_releaseA hold that was blocking deletion was released.

Your action does not vary by reason. It is on the list so you can log why, and so a takedown is distinguishable from a person’s own request when someone asks you about it later.

A notice with no workspace obliges everyone

Acting on resources

resources names the handles to delete — the same handles you stored when you mirrored the data, so this is a direct lookup rather than a search. Match on object plus id.

An erasure consumer
def consume_erasures(session, store):
    """Honour every suppression notice. Run at least daily."""
    cursor = store.load_erasure_cursor()

    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        body = session.get(f"{BASE}/erasures", params=params, timeout=30).json()

        for notice in body["data"]:
            # The obligation attaches to the SUPPRESSION. erasure_completed is
            # Tahoe confirming its own destruction, which can be years later
            # because of statutory retention — waiting for it would mean holding
            # the data long after the person asked us to stop.
            if notice["action"] == "suppression_applied":
                for resource in notice["resources"]:
                    store.purge(resource["object"], resource["id"])
                # A pool suppression carries no workspace and still obliges us,
                # so never filter this feed by workspace_id.
                store.record_honoured(notice["id"], notice["deadline_at"])

        if body["data"]:
            cursor = body["next_cursor"]
            store.save_erasure_cursor(cursor)

        if not body["has_more"]:
            return

The same notices arrive as events too

data_subject.suppression_applied and data_subject.erasure_completed are on the change feed, so a consumer that already drains that feed can act without a second integration. This endpoint exists so the notices can be audited and replayed on their own, independently of your event watermark — if your event consumer has been down, this is how you catch up on the ones that matter most.