# Purge semantics (/guides/purge-semantics)



Deletion in Zumik is explicit, profile-specific, and verifiable. There are two distinct operations,
and they make different promises.

## Delete is not purge [#delete-is-not-purge]

| Operation                         | Promise                                                                                                  |
| --------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `DELETE /v2/artifacts/{id}`       | Revoke the handle immediately. The id stops resolving.                                                   |
| `POST /v2/purge-jobs`             | Remove the retained state, invalidate dependent caches, and emit a signed receipt of what was confirmed. |
| `GET /v2/purge-jobs/{id}/receipt` | Retrieve the receipt with per-processor evidence.                                                        |

A delete makes the artifact unusable through the API. A purge goes further: it physically removes
the stored bytes, bumps the isolation-namespace generation so any cached realization is poisoned, and
records cryptographic evidence of the result. Use delete for routine cleanup; use purge when you need
to *prove* state is gone - a data-subject erasure, a leaked secret, an end-of-contract teardown.

## Run a purge [#run-a-purge]

`POST /v2/purge-jobs` with the artifacts in scope. Each must be an artifact in your project.

```bash
curl https://api.zumik.ai/v2/purge-jobs \
  -H "Authorization: Bearer zk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"artifact_ids":["art_01jy…"]}'
```

```json title="Job"
{
  "id": "pjb_01jy…",
  "object": "purge_job",
  "status": "completed",
  "scope": { "project_id": "prj_01jy…", "artifact_ids": ["art_01jy…"] },
  "requested_at": "2026-06-15T12:00:00Z"
}
```

Under the hood a purge job revokes the handles, tombstones the artifacts and bundles, invalidates the
sessions and snapshots that depend on them, increments the namespace purge generation, removes the
canonical blobs and materializations, drops retained traces per the project's trace mode, invalidates
runtime cache namespaces, requests provider-side deletion where supported, records each processor's
acknowledgment, and issues the receipt.

## The receipt [#the-receipt]

`GET /v2/purge-jobs/{id}/receipt` returns the evidence. It is keyed by the *job* id.

```json
{
  "id": "pur_01jy…",
  "object": "purge_receipt",
  "requested_at": "2026-06-15T12:00:00Z",
  "completed_at": "2026-06-15T12:00:10Z",
  "scope": { "project_id": "prj_01jy…", "artifact_ids": ["art_01jy…"] },
  "guarantee": "verified_physical_purge",
  "processors": [ { "name": "state_store", "status": "purged" } ],
  "receipt_digest": "sha256:9f86d081…"
}
```

The `receipt_digest` is a recomputable SHA-256 over the job id, project id, the new namespace
generation, every artifact id, and the completion time. It is evidence you can verify, not a marketing
checkmark.

## Guarantee classes [#guarantee-classes]

The receipt's `guarantee` is the **weakest** class any in-scope processor reported - a chain is only
as strong as its weakest link. From weakest to strongest:

| Class                             | Meaning                                                                                        |
| --------------------------------- | ---------------------------------------------------------------------------------------------- |
| `access_revoked`                  | Customer handles stop working immediately.                                                     |
| `best_effort_expiry`              | A provider-controlled cache expires under the provider's own policy; `expires_at` is reported. |
| `verified_namespace_invalidation` | The namespace generation moved, so any stale physical cache can no longer be reused.           |
| `verified_physical_purge`         | Managed storage and cache processors confirmed deletion of the bytes.                          |
| `cryptographic_purge`             | Key destruction makes encrypted retained bytes permanently unreadable.                         |

A purge of an artifact held only in Zumik's own store, with no BYOC runtime attached, reaches
`verified_physical_purge` - the state store reports `purged` and there is no weaker processor to drag
the guarantee down. When a self-hosted runtime [profile](/concepts/execution-profiles) is in scope, a
`byoc_runtime` processor appears and the guarantee reflects what that runtime could confirm.

<Note>
  The `byoc_runtime` processor is only added when the project has a live BYOC cluster. Zumik never
  fabricates a processor for infrastructure that isn't running, so the guarantee always reflects real
  acknowledgments.
</Note>

## Provider-profile limitation [#provider-profile-limitation]

For managed-provider traffic, Zumik can revoke access and invalidate its own caches, but it cannot
force a third-party provider to physically erase a cache faster than that provider's policy allows.
Those processors report `best_effort_expiry` with a concrete `expires_at`. The receipt states this
honestly rather than overclaiming a physical purge it cannot verify.

## Resurrection prevention [#resurrection-prevention]

The point of bumping the namespace generation is that a purged artifact can never come back to life
from a cache that outlived it. Physical caches are keyed by generation, so after a purge every prior
realization is unreachable - a stale KV block from before the purge will never be served. This is the
mechanism behind the `purge_verification` [replay class](/guides/replay): you can replay traffic that
referenced a purged artifact and confirm it is not reusable.

<Card title="Verify erasure with replay" icon="shield-check" href="/guides/replay">
  Run a purge\_verification replay to prove invalidated artifacts cannot be reused.
</Card>
