---
title: "Revoke proof"
method: POST
path: "/api/v1/proofs/{id}/revoke"
tags: ["Proofs"]
---

# Revoke proof

`POST /api/v1/proofs/{id}/revoke`

## Request

- Base URL: `https://api.proof.holdings`
- URL: `https://api.proof.holdings/api/v1/proofs/{id}/revoke`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `auth_token`

Revoke a proof token, making it invalid. Accepts the public proof handle (`ph_ctl_<32 hex>` or `ph_dlg_<32 hex>`) or a verification id — the same addresses the status, suspend and unsuspend routes take. Revoking a PROOF does not withdraw the asset, consent or delegation it is about: those have their own surfaces. The revocation reaches the registry row, `GET /proofs/{id}/status`, `POST /proofs/validate`, the revocation list and the Token Status List. One difference between the two addresses: by verification id a proof whose verification is not `verified` answers `not_verified`, while by handle it is revoked anyway — the registry row is live and would otherwise keep validating — leaving the verification in whatever state it really ended in. Reachable with an API key (`proofs:write`) or a dashboard session cookie; the scope binds API-key callers only. A CONTROL proof — a domain proof delegations can hang off (`ph_ctl_*` of a domain asset or verification, or a domain verification id) — takes the gated path (h-implement-proper-deletion, DOM-3): for a domain the account OWNS the whole-domain cascade runs (every proof of the domain and every delegation issued from it revoked, the domain demoted to `reverification_required` with its DNS challenge rotated — the same act as `DELETE /me/assets/{id}` on the domain asset); for a bare verification of somebody else's domain only that proof and the delegations anchored on it are revoked, sibling verifications stay live. The body then carries `principal`, `owned_domain`, `domain_status` and `delegations_revoked`. A repeat answers 200 while something was left to re-drive (a delegation still live under a revoked proof), else `400 already_revoked` — decided before any step-up code is spent. A handle revoked BEFORE the domain was proven again answers `400 already_revoked` and never reaches the new proof: revoke that one by its own handle. An API key revoking a control proof, or one of the account's own phone or email proofs, has the act recorded for the hourly owner digest (one digest per key).

## Path parameters

- `id` string, required: Public proof handle (`ph_ctl_<32 hex>` / `ph_dlg_<32 hex>`) or verification id

## Request body

- RevokeProofRequest: Request body for revoking a proof
  - `reason` 'user_request' | 'fraud' | 'key_compromise' | 'asset_transferred' | 'other': Reason for revoking a proof

## Response `200`

Proof revoked

- RevokeProofResponse
  - `success` boolean, required
  - `proof_id` string: Public proof handle (`ph_ctl_*`/`ph_dlg_*`). Present when the proof was addressed by handle.
  - `proof_type` string: Which record carries the revoked proof (`verification`, `user_asset`, `verification_request`, `delegation`, …). Present when the proof was addressed by handle.
  - `verification_id` string: Present only when the revoked proof belongs to a verification.
  - `revoked_at` string, date-time, required
  - `reason` string, required
  - `principal` string, nullable: Control proofs only: the domain the proof is about.
  - `owned_domain` boolean: Control proofs only: true when the account OWNS the domain (a Domain row or a domain asset for the name) and the whole-domain cascade ran — every proof of the domain and every delegation issued from it revoked, the domain demoted to `reverification_required`. False for a bare verification of somebody else's domain: that proof and the delegations anchored on it only.
  - `domain_status` string, nullable: Control proofs only: the Domain row's status after the act (`reverification_required` after the cascade); null when the account has no Domain row for the name.
  - `delegations_revoked` object[]: Control proofs only: the delegations revoked with the proof. Re-verifying the domain does not bring them back.
    - `proof_id` string, required
    - `delegate` string, required
    - `delegate_type` string, required
    - `principal` string, required

## Other responses

- `400`: Already revoked, not in a revocable state, or a revocation reason this proof type does not accept. `already_revoked` applies to a PROOF only: a repeat revoke of a DELEGATION handle (`ph_dlg_*`) answers 200 with the stored revocation instead, matching `POST /delegations/{id}/revoke`, whose contract has always advertised idempotency. The proof code stays as it is because it is published and read by integrators
- `403`: `account_key_required` — a key bound to one sub-account cannot change the proof of a shared asset (a phone, email or domain asset, a delegation, or a domain control proof of one of the account's own domains or with live delegations under it); it can read its status. Any other domain proof of its own sub-account — a customer's domain verified through its key — it may change. A proof of another sub-account answers 404. For an API-key caller, acting on a delegation handle (`ph_dlg_*`) additionally requires the `delegations:write` scope, so it keeps the gate its own route puts on it (`insufficient_scope`). The same rule covers suspend and unsuspend for that handle: a pause takes the delegation offline for every verifier while it holds, which is what a revocation does, and being reversible bounds only the cost of putting it back. For a JWT dashboard session (which carries no scope), the same act instead requires a live `delegation_lifecycle` 2FA step-up grant (`403 2fa_required` — start one via `POST /me/2fa/start` with `action_type: delegation_lifecycle`), gated identically for revoke, suspend and unsuspend. For a CONTROL proof (a domain proof): an API key needs `delegations:write` as well while the act reaches live delegations — for revoke, suspend and unsuspend alike, since a pause hides the delegations exactly as a revocation does; a revoke of a domain the account OWNS reaches every live delegation of the domain, whichever of its handles was named — and the `insufficient_scope` body names `missing_scope` and `live_delegations`; a session needs, for REVOKE only, a consumed single-use `revocation_bulk` step-up grant bound to the id it sent (`2fa_required`; `reason: target_mismatch` when the grant names other ids) — the same code the dashboard's own routes demand, so this is not a 2FA-free address for the permanent cascade. A pause needs no step-up. A session revoking one of the account's OWN phone or email proofs (an asset proof, by its `ph_ctl_*` handle) needs the same `revocation_bulk` grant bound to that handle; a proof that is already revoked answers `400 already_revoked` without one, and an API key keeps its `proofs:write` rule.
- `404`: Proof not found
- `409`: `domain_revoking` — a revocation of this domain is already running; nothing was changed. Retry in a moment. Normally no step-up code is spent; a code is spent only when the other revocation took the domain in the instant between this request's check and its own start
- `503`: The revocation could not be written — retry (`proof_revocation_failed`)

---

[API](https://skmtc.dev/proof/apis/proof-holdings-api.md) · [All operations](https://skmtc.dev/proof/apis/proof-holdings-api/llms.txt) · [OpenAPI document](https://skmtc.dev/proof/apis/proof-holdings-api/revisions/6b8d61ee354f?raw)
