Skip to content

identity history API

Identity Operation History

Every mutation on an identity automatically records a before/after snapshot. Use undo/redo to revert or reapply changes, and history to inspect the operation log.

Three independent undo/redo systems exist:

System Storage Operations Covered
PATCH PostgreSQL identity_history update
Bind PostgreSQL identity_history bind, unbind, bind_trace
Merge MongoDB identity_merge_history mergeinto
Delete PostgreSQL identity_history delete

1. PATCH History & Undo/Redo

Overview

Property Value
Storage PostgreSQL identity_history table
Snapshot Full identity record (all fields) before and after each PATCH
Max records 256 per identity (oldest auto-deleted when limit exceeded)
Undo steps Unlimited (no expiry, no step limit)
Redo stack Cleared on new PATCH (is_undone=true + operation='update' records are deleted)
Stack Model
PATCH 1 → PATCH 2 → PATCH 3         (undo stack, is_undone=false)
                           ↓ undo
PATCH 1 → PATCH 2                   (undo stack)
           PATCH 3                   (redo stack, is_undone=true)
                           ↓ redo
PATCH 1 → PATCH 2 → PATCH 3         (undo stack)

A new PATCH after undo clears only the operation='update' redo stack (PATCH 3 is lost). Bind/merge redo stacks are not affected.


POST /api/v1/identity/:identity_uuid/undo

Auth: Required Scope: identity-level

Undo the most recent PATCH operations. Restores the identity's before_snapshot and marks the history records as undone.

Request (JSON)
Field Type Required Default Description
steps integer No 1 Number of undo steps to apply (max records undone in one call)
Behavior
  • Queries is_undone=false records with operation='update', ordered by created_at DESC
  • Restores name, identity_type, source, status, metadata, tmdb_id, tmdb_profile from the last record's before_snapshot
  • Marks the undone records as is_undone=true with undone_at=NOW()
  • Syncs identity.json to disk
  • Updates _index.json if name changed
Example
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/undo" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"steps": 1}'
Response (200)
{
  "success": true,
  "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
  "undone_count": 1,
  "current_state": {
    "id": 9,
    "uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
    "name": "Cary Grant",
    "identity_type": "people",
    "source": "tmdb",
    "status": "confirmed",
    "metadata": {},
    "tmdb_id": 112,
    "tmdb_profile": null
  }
}
Field Type Description
undone_count integer Number of history records undone
current_state object Full identity state after undo
Error Responses
HTTP When
400 No undo operations available
404 Identity not found
500 Database error

POST /api/v1/identity/:identity_uuid/redo

Auth: Required Scope: identity-level

Redo previously undone PATCH operations. Restores the identity's after_snapshot and marks the history records as no longer undone.

Request (JSON)
Field Type Required Default Description
steps integer No 1 Number of redo steps to apply
Behavior
  • Queries is_undone=true records with operation='update', ordered by created_at DESC
  • Restores all identity fields from the last record's after_snapshot
  • Marks records as is_undone=false with undone_at=NULL
  • Syncs identity.json to disk
  • Updates _index.json if name changed
Example
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/redo" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"steps": 1}'
Response (200)
{
  "success": true,
  "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
  "redone_count": 1,
  "current_state": {
    "id": 9,
    "uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
    "name": "John Smith",
    "identity_type": "people",
    "source": "tmdb",
    "status": "confirmed",
    "metadata": { "aliases": [...] },
    "tmdb_id": 112,
    "tmdb_profile": null
  }
}
Field Type Description
redone_count integer Number of history records redone
current_state object Full identity state after redo
Error Responses
HTTP When
400 No redo operations available
404 Identity not found
500 Database error

GET /api/v1/identity/:identity_uuid/history

Auth: Required Scope: identity-level

Query the PATCH operation history for an identity. Returns paginated records with undo/redo stack counts (filtered to operation='update').

Query Parameters
Field Type Required Default Description
page integer No 1 Page number (1-indexed)
limit integer No 20 Items per page (max 100)
Response (200)
{
  "success": true,
  "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
  "total": 5,
  "undo_stack_count": 3,
  "redo_stack_count": 2,
  "results": [
    {
      "history_id": 42,
      "operation": "update",
      "is_undone": false,
      "created_at": "2026-05-27T12:00:00Z",
      "undone_at": null
    },
    {
      "history_id": 41,
      "operation": "update",
      "is_undone": true,
      "created_at": "2026-05-27T11:30:00Z",
      "undone_at": "2026-05-27T13:00:00Z"
    }
  ]
}
Field Type Description
total integer Total PATCH history records for this identity
undo_stack_count integer Records available for undo (is_undone=false)
redo_stack_count integer Records available for redo (is_undone=true)
results[].history_id integer History record ID
results[].operation string Operation type ("update" for PATCH)
results[].is_undone boolean Whether the operation has been undone
results[].created_at string When the PATCH was applied
results[].undone_at string When the undo occurred (null if not undone)
Example
curl -s "$API/api/v1/identity/$IDENTITY_UUID/history?page=1&limit=10" \
  -H "X-API-Key: $KEY"
Error Responses
HTTP When
404 Identity not found
500 Database error

2. Bind/Unbind/Trace History & Undo/Redo

All three operations (bind, unbind, bind_trace) share a single history table and undo/redo stack.

Bind Operation Overview

Property Value
Storage PostgreSQL identity_history table (same table as PATCH)
Snapshot {"file_uuid", "face_id" (or "trace_id"), "identity_id_before/after"}
Max records 256 per identity (shared limit across all operation types)
Undo steps Unlimited (steps param)
Redo stack Cleared on new bind/unbind/bind_trace (operation IN ('bind','unbind','bind_trace') + is_undone=true records deleted)
Stack isolation Bind redo stack is independent from PATCH redo stack — clearing one does not affect the other
Stack Model
bind face_1 (to id=9)              → unbind face_1          → bind trace 906 (to id=9)
(undo stack, is_undone=false)         (undo stack)              (undo stack)
                                                               ↓ undo (first undone: bind_trace)
                                     bind trace 906 (is_undone=true)
                                     (redo stack)
                                                               ↓ redo
bind face_1 → unbind face_1 → bind trace 906
(undo stack)

A new bind/unbind/trace after undo clears only the bind redo stack (operations with IN ('bind','unbind','bind_trace')).

Snapshot Format

Before (bind):

{
  "file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
  "face_id": "1_5",
  "identity_id_before": null
}

After (bind):

{
  "file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
  "face_id": "1_5",
  "identity_id_after": 9
}

Before (unbind) — binding existed before:

{
  "file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
  "face_id": "1_5",
  "identity_id_before": 9
}

After (unbind):

{
  "file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
  "face_id": "1_5",
  "identity_id_after": null
}

For bind_trace, the snapshot uses trace_id instead of face_id, with identity_id_before capturing the first face's identity in that trace.


POST /api/v1/identity/:identity_uuid/bind/undo

Auth: Required Scope: identity-level

Undo the most recent bind/unbind/bind_trace operations. Restores identity_id_before from the snapshot and marks records as undone.

Request (JSON)
Field Type Required Default Description
steps integer No 1 Number of undo steps to apply
Behavior
  • Queries is_undone=false records with operation IN ('bind','unbind','bind_trace'), ordered by created_at DESC
  • Restores identity_id_before — for bind this is null (face was unbound), for unbind this is the original identity (face goes back), for bind_trace this is the trace's previous identity
  • Marks the undone records as is_undone=true with undone_at=NOW()
Example
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/undo" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"steps": 1}'
Response (200)
{
  "success": true,
  "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
  "operation": "bind",
  "undone_count": 1,
  "affected_rows": 53
}
Field Type Description
operation string The actual operation undone (bind, unbind, or bind_trace)
undone_count integer Number of history records undone
affected_rows integer Number of face_detections rows updated
Error Responses
HTTP When
400 No bind undo operations available
404 Identity not found
500 Database error

POST /api/v1/identity/:identity_uuid/bind/redo

Auth: Required Scope: identity-level

Redo previously undone bind/unbind/bind_trace operations. Restores identity_id_after from the snapshot.

Request (JSON)
Field Type Required Default Description
steps integer No 1 Number of redo steps to apply
Behavior
  • Queries is_undone=true records with operation IN ('bind','unbind','bind_trace'), ordered by created_at DESC
  • Restores identity_id_after — for bind this is the identity the face was bound to, for unbind this is null
  • Marks records as is_undone=false with undone_at=NULL
Example
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/redo" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"steps": 1}'
Response (200)
{
  "success": true,
  "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
  "operation": "unbind",
  "redone_count": 1,
  "affected_rows": 1
}
Field Type Description
operation string The actual operation redone (bind, unbind, or bind_trace)
redone_count integer Number of history records redone
affected_rows integer Number of face_detections rows updated
Error Responses
HTTP When
400 No bind redo operations available
404 Identity not found
500 Database error

GET /api/v1/identity/:identity_uuid/bind/history

Auth: Required Scope: identity-level

Query the bind/unbind/bind_trace operation history for an identity. Returns paginated records with undo/redo stack counts.

Query Parameters
Field Type Required Default Description
page integer No 1 Page number (1-indexed)
limit integer No 20 Items per page (max 100)
Response (200)
{
  "success": true,
  "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
  "total": 3,
  "undo_stack_count": 2,
  "redo_stack_count": 1,
  "results": [
    {
      "history_id": 52,
      "operation": "bind_trace",
      "is_undone": false,
      "created_at": "2026-05-27T14:00:00Z",
      "undone_at": null
    },
    {
      "history_id": 51,
      "operation": "unbind",
      "is_undone": true,
      "created_at": "2026-05-27T13:00:00Z",
      "undone_at": "2026-05-27T14:30:00Z"
    },
    {
      "history_id": 50,
      "operation": "bind",
      "is_undone": false,
      "created_at": "2026-05-27T12:00:00Z",
      "undone_at": null
    }
  ]
}
Field Type Description
total integer Total bind history records for this identity
undo_stack_count integer Records available for undo (is_undone=false)
redo_stack_count integer Records available for redo (is_undone=true)
results[].history_id integer History record ID
results[].operation string Operation type (bind, unbind, or bind_trace)
results[].is_undone boolean Whether the operation has been undone
results[].created_at string When the operation was applied
results[].undone_at string When the undo occurred (null if not undone)
Example
curl -s "$API/api/v1/identity/$IDENTITY_UUID/bind/history?page=1&limit=10" \
  -H "X-API-Key: $KEY"
Error Responses
HTTP When
404 Identity not found
500 Database error

3. Merge History & Undo/Redo

Merge operations use MongoDB for richer record-keeping, with a 24-hour undo deadline.

Merge Operation Overview

Property Value
Storage MongoDB identity_merge_history collection
Snapshot Full source identity state + target identity state + aliases/metadata diffs
Trigger Every mergeinto with keep_history=true
Undo deadline 24 hours (renewed on redo)
Redo support Yes — restores undone merges with new 24hr deadline
Max records Unlimited

POST /api/v1/identity/merge/:merge_id/undo

Already documented in 07_identity.md. See that document for full details.


POST /api/v1/identity/merge/:merge_id/redo

Auth: Required Scope: identity-level

Redo a previously undone merge operation within the renewed 24-hour deadline.

Request

No body required. The merge ID is taken from the URL path.

Behavior
  1. Validates the merge record exists and undone=true (not already active)
  2. Checks the 24-hour undo deadline (if expired, the redo is rejected)
  3. Restores face bindings: moves all faces from target_identity back to source_identity
  4. Re-adds aliases that were removed by the undo (aliases with source: "merge" tag)
  5. Re-adds metadata fields that were removed by the undo
  6. If keep_history=true: sets source_identity.status = 'merged' again
  7. If keep_history=false: recreates source identity from the undone_snapshot stored at undo time
  8. Syncs both identity JSON files to disk
  9. Sets undone=false, clears undone_snapshot, renews undo_deadline = NOW() + 24h
  10. Records redone_by user for audit
Example
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/redo" \
  -H "X-API-Key: $KEY"
Response (200)
{
  "success": true,
  "message": "Redo merge completed: merged 'stranger_13894' into 'Louis Viret' (52 faces transferred)",
  "data": {
    "merge_id": "550e8400-e29b-41d4-a716-446655440000",
    "faces_transferred": 52,
    "aliases_re_added": 1,
    "metadata_fields_re_added": 2
  }
}
Field Type Description
merge_id string The merge operation ID
faces_transferred integer Number of faces transferred from source to target
aliases_re_added integer Number of aliases restored to target
metadata_fields_re_added integer Number of metadata fields restored to target
Error Responses
HTTP When
400 Merge not undone, deadline expired, or cannot redo
404 Merge record not found
500 Database error

4. Delete History & Undo/Redo

Delete Operation Overview

Property Value
Storage PostgreSQL identity_history table
Snapshot {"identity": {...full row...}, "unbound_faces": [{file_uuid, face_id, trace_id}, ...]}
Max records 1 active delete record per identity (redo stack cleared on new delete)
Undo support Yes — recreates identity row, re-binds faces
Redo support Yes — re-deletes the identity
Identity file Deleted on delete, recreated on undo

Snapshot Format

{
  "identity": {
    "id": 9,
    "uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
    "name": "Cary Grant",
    "identity_type": "people",
    "source": "tmdb",
    "status": "confirmed",
    "metadata": {},
    "tmdb_id": 112,
    "tmdb_profile": null
  },
  "unbound_faces": [
    {
      "file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
      "face_id": "1_5",
      "trace_id": null
    },
    {
      "file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
      "face_id": "1_6",
      "trace_id": 906
    }
  ]
}

Stack Model

DELETE identity                          (undo stack, is_undone=false)
               ↓ undo
Identity recreated, faces re-bound
               → delete history marked is_undone=true
               ↓ redo (re-delete)
Identity deleted again, faces unbound
               → delete history marked is_undone=false

A new delete after an undo clears the delete redo stack (no redo possible for the old delete).

Undo Behavior (via existing POST /api/v1/identity/:identity_uuid/undo)

  1. Normal identity lookup fails (row was deleted)
  2. Checks identity_history for operation='delete' AND is_undone=false matching the UUID in the snapshot
  3. Recreates the identity row (new internal id, same UUID)
  4. Re-binds all faces listed in unbound_faces to the new identity
  5. Deletes the identity_history delete record as is_undone=true with undone_at=NOW()
  6. Syncs identity.json to disk
  7. Updates _index.json

Redo Behavior (via existing POST /api/v1/identity/:identity_uuid/redo)

  1. Identity lookup succeeds (identity was restored by prior undo)
  2. Checks identity_history for operation='delete' AND is_undone=true matching the identity_id
  3. Deletes identity.json from disk
  4. Unbinds all faces (identity_id = NULL)
  5. Deletes the identity row
  6. Marks the delete history record as is_undone=false
  7. Returns success

Error Responses (delete undo/redo)

HTTP Scenario
400 No delete history available (either no delete or already undone/redone)
404 Identity not found (for redo — identity wasn't restored)
500 Database error

Comparison: PATCH vs Bind vs Merge vs Delete Undo/Redo

Aspect PATCH Undo/Redo Bind Undo/Redo Merge Undo/Redo Delete Undo/Redo
Storage PostgreSQL identity_history PostgreSQL identity_history MongoDB identity_merge_history PostgreSQL identity_history
Operation filter operation='update' operation IN ('bind','unbind','bind_trace') — operation='delete'
Trigger Every PATCH Every bind/unbind/bind_trace Every mergeinto with keep_history=true Every DELETE
Undo deadline None (unlimited) None (unlimited) 24 hours (renewed on redo) None (unlimited)
Redo support Yes Yes Yes Yes
Step undo Yes (steps param) Yes (steps param) No (full undo/redo only) No (single record)
Max records 256 per identity 256 per identity (shared) Unlimited 256 per identity (shared)
User tracking user_id + user_source user_id + user_source performed_by_user + undone_by / redone_by user_id + user_source

Updated: 2026-05-28


Version History

Version Date Changes
1.0 2026-10-24 Initial documentation