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=falserecords withoperation='update', ordered bycreated_at DESC - Restores
name,identity_type,source,status,metadata,tmdb_id,tmdb_profilefrom the last record'sbefore_snapshot - Marks the undone records as
is_undone=truewithundone_at=NOW() - Syncs
identity.jsonto disk - Updates
_index.jsonif 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=truerecords withoperation='update', ordered bycreated_at DESC - Restores all identity fields from the last record's
after_snapshot - Marks records as
is_undone=falsewithundone_at=NULL - Syncs
identity.jsonto disk - Updates
_index.jsonif 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¶
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):
After (bind):
Before (unbind) — binding existed before:
After (unbind):
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=falserecords withoperation IN ('bind','unbind','bind_trace'), ordered bycreated_at DESC - Restores
identity_id_before— for bind this isnull(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=truewithundone_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=truerecords withoperation IN ('bind','unbind','bind_trace'), ordered bycreated_at DESC - Restores
identity_id_after— for bind this is the identity the face was bound to, for unbind this isnull - Marks records as
is_undone=falsewithundone_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¶
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¶
- Validates the merge record exists and
undone=true(not already active) - Checks the 24-hour undo deadline (if expired, the redo is rejected)
- Restores face bindings: moves all faces from
target_identityback tosource_identity - Re-adds aliases that were removed by the undo (aliases with
source: "merge"tag) - Re-adds metadata fields that were removed by the undo
- If
keep_history=true: setssource_identity.status = 'merged'again - If
keep_history=false: recreates source identity from theundone_snapshotstored at undo time - Syncs both identity JSON files to disk
- Sets
undone=false, clearsundone_snapshot, renewsundo_deadline = NOW() + 24h - Records
redone_byuser 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)¶
- Normal identity lookup fails (row was deleted)
- Checks
identity_historyforoperation='delete' AND is_undone=falsematching the UUID in the snapshot - Recreates the identity row (new internal
id, same UUID) - Re-binds all faces listed in
unbound_facesto the new identity - Deletes the
identity_historydelete record asis_undone=truewithundone_at=NOW() - Syncs
identity.jsonto disk - Updates
_index.json
Redo Behavior (via existing POST /api/v1/identity/:identity_uuid/redo)¶
- Identity lookup succeeds (identity was restored by prior undo)
- Checks
identity_historyforoperation='delete' AND is_undone=truematching the identity_id - Deletes
identity.jsonfrom disk - Unbinds all faces (
identity_id = NULL) - Deletes the identity row
- Marks the delete history record as
is_undone=false - 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 |