---
title: "Approve Identity Case"
method: POST
path: "/v1/admin/identity-cases/{case_id}/approve"
tags: ["admin"]
---

# Approve Identity Case

`POST /v1/admin/identity-cases/{case_id}/approve`

## Request

- Base URL: `https://api.patricia.app` (the document declares no server; this is the origin it was published from)
- URL: `https://api.patricia.app/v1/admin/identity-cases/{case_id}/approve`
- Auth: HTTP bearer

Record the decision and its server-computed digest, moving the case to ready.

The digest is computed here, from the document the approver submitted. Accepting one from the
caller would authenticate nothing — the same request supplies the document it summarizes.

## Path parameters

- `case_id` string, uuid, required

## Request body

- ManifestIn: The decision, as the approver writes it down. Every collection is BOUNDED, and the reason is the commit rather than the request. The ceremony is one transaction that locks each participant at stage 70 and then every named row in turn, and it holds all of them until it returns — so the length of these lists is the length of time a recovery blocks every other writer that needs one of those users. Unbounded, a generated document naming a whole tenant's rows is a lock storm nobody typed on purpose. The bounds are orders of magnitude above a real recovery, which reconciles two humans and names a handful of rows; hitting one is a 422 an operator can read, not a database that has gone quiet.
  - `survivor_user_id` string, uuid, required
  - `survivor_email` string, required: The surviving human's address, as the legacy readers know it.
  - `participants` ParticipantIn[], required
    - `user_id` string, uuid, required
    - `expected_credential_epoch` integer, required
  - `memberships` MembershipRowIn[]: NOT YET SUPPORTED — must be empty. The recovery ceremony moves credential rows and never memberships, so a non-empty value is refused at approve with `manifest_memberships_unsupported`. The field is published because the document is the whole decision and its digest binds all of it; converging memberships is a later manifest-bound transition.
    - `membership_id` string, uuid, required
    - `disposition` 'approved' | 'hostile', required: What the approver decided about one row. Two answers, and no default. There is deliberately no "unknown" member. A row the approver did not classify is a row the manifest does not name, and a row the manifest does not name is never touched — absence is the safe state, so representing indecision inside the document would create a third behaviour nobody approved.
  - `integration_accounts` CredentialRowIn[]
    - `row_id` string, uuid, required
    - `owner` OwnerIn, required: The owner a credential row carries TODAY, as the approver read it off the case. Mirrors the four-variant sum type the credential plane writes through. It is stated rather than inferred because the recovery's per-variant rule depends on it, and because the commit checks this claim against the live row — a document describing a row that has since changed must fail rather than move it.
      - `variant` 'dashboard' | 'chat' | 'system' | 'unresolved', required
      - `user_id` string, uuid, nullable
      - `actor_id` string, uuid, nullable
      - `email` string, nullable
      - `bridged_actor_id` string, uuid, nullable
      - `reason` 'actor_not_in_tenant' | 'ambiguous_verified_email' | 'no_principal' | 'team_scope' | 'legacy_row': Why a writer could not name an owner. Recorded for the pre-flip inventory, never for control flow. A reason is an OBSERVATION about one write. It never widens what the row may do: every unresolved row lands on the same fail-closed columns whichever reason produced it.
    - `disposition` 'approved' | 'hostile', required: What the approver decided about one row. Two answers, and no default. There is deliberately no "unknown" member. A row the approver did not classify is a row the manifest does not name, and a row the manifest does not name is never touched — absence is the safe state, so representing indecision inside the document would create a third behaviour nobody approved.
  - `pending_connections` CredentialRowIn[]
    - `row_id` string, uuid, required
    - `owner` OwnerIn, required: The owner a credential row carries TODAY, as the approver read it off the case. Mirrors the four-variant sum type the credential plane writes through. It is stated rather than inferred because the recovery's per-variant rule depends on it, and because the commit checks this claim against the live row — a document describing a row that has since changed must fail rather than move it.
      - `variant` 'dashboard' | 'chat' | 'system' | 'unresolved', required
      - `user_id` string, uuid, nullable
      - `actor_id` string, uuid, nullable
      - `email` string, nullable
      - `bridged_actor_id` string, uuid, nullable
      - `reason` 'actor_not_in_tenant' | 'ambiguous_verified_email' | 'no_principal' | 'team_scope' | 'legacy_row': Why a writer could not name an owner. Recorded for the pre-flip inventory, never for control flow. A reason is an OBSERVATION about one write. It never widens what the row may do: every unresolved row lands on the same fail-closed columns whichever reason produced it.
    - `disposition` 'approved' | 'hostile', required: What the approver decided about one row. Two answers, and no default. There is deliberately no "unknown" member. A row the approver did not classify is a row the manifest does not name, and a row the manifest does not name is never touched — absence is the safe state, so representing indecision inside the document would create a third behaviour nobody approved.
  - `connector_repairs` ConnectorRepairIn[]
    - `row_id` string, uuid, required
    - `candidate_user_id` string, uuid, required
    - `proof_kind` string, required
    - `proof_reference` string, required
    - `expected_connected_by_actor_id` unknown, required
    - `expected_connected_by_user_id` unknown, required
    - `expected_connected_by_email` string, required
    - `expected_owner_origin_code` 'legacy_unknown', required
    - `expected_owner_link_state_code` 'unresolved', required
    - `expected_provider_partition_user_id` string, nullable, required

## Response `200`

Successful Response

- CaseRow: One case in the operator queue: the identifiers, plus what a human needs to recognise them. The identifiers are the record and the label beside them is orientation. The candidate's name is optional and mutable — an account is deleted and its name goes with it — so it is served next to the stable id it describes rather than in place of it, and a missing label says so instead of rendering blank. **This row carries the captured address, and that reverses an earlier decision.** It used to serve only a boolean saying whether that address matched the candidate's, on the reasoning that this queue spans every workspace and an address is the one field identifying somebody outside our own identifier space. That reasoning is sound and it was overruled on 2026-08-24: without the address an operator cannot answer the question the screen exists to ask, so the queue served six opaque ids and no way to act on them. The disclosure carries its own cost instead — the list route sets ``no_store`` and commits a platform read-audit row before any address is returned.
  - `id` string, uuid, required
  - `status_code` string, required
  - `reason_code` string, required
  - `target_tenant_id` string, uuid, nullable, required
  - `candidate_user_id` string, uuid, nullable, required
  - `created_at` string, date-time, required
  - `expires_at` string, date-time, required
  - `has_approved_manifest` boolean, required: Whether this case carries a decision that commit would accept: both the stored document and its digest are present.
  - `issuer` string, nullable
  - `subject` string, nullable
  - `subject_scope` string, nullable
  - `platform_team_id` string, nullable
  - `platform_actor_id` string, nullable
  - `expected_credential_epoch` integer, nullable
  - `incoming_email` string, nullable
  - `candidate_display_name` string, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/patricia/apis/patricia-api.md) · [All operations](https://skmtc.dev/patricia/apis/patricia-api/llms.txt) · [OpenAPI document](https://skmtc.dev/patricia/apis/patricia-api/revisions/0dfb44495ea1?raw)
