Legal Holds API
The legal holds API sets, lists, and releases legal holds: named, audited instructions that block deletion of redaction evidence until the hold is released.
Admin cross-user access: by default each endpoint operates on the calling user's own holds. An admin may target another user by adding an
owner=<username>query parameter to any endpoint (set, list, get, release). A non-admin that names another user asowner, or anownerthat does not exist, receives404 Not Found. Cross-user access is disabled by default; enable it withADMIN_CROSS_USER_ACCESS_ENABLED=true(see Settings). While disabled, naming another user asowneralso returns404 Not Found. A deactivated user may be named asowner: deactivation keeps their data, and an admin reaches it as for an active user.
All endpoints require authentication with a Bearer token. See API Keys.
Set a Hold
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/holds |
Create a named hold that blocks deletion of evidence. |
Request Body
| Field | Required | Description |
|---|---|---|
reference |
Yes | Hold identifier, unique for the calling user (for example LIT-2026-001). Philter does not interpret it, but it is how the hold is read and released, in a request path, so it cannot contain /, \, ;, %, or control characters, and cannot be . or ... Other text, including spaces and periods, is allowed. |
scopeType |
Yes | document_chain to protect one document's ledger chain, or user to protect all of the hold owner's ledger evidence. The owner is the caller, or the user named by owner. |
scopeValue |
For document_chain |
For document_chain, the document id. For user, optional: the hold already covers everything its owner holds. If given, it must be the owner's username, or the request is refused with 400 Bad Request. Philter stores and returns the owner's username for a user hold either way. |
reason |
No | Free-text description of why the hold was set. |
Query Parameters
owner(optional, admin only) - Username of the user whose evidence the hold protects.
Responses
201 Created- The hold was set and is now active. The body is the hold.400 Bad Request- A required field is missing,scopeTypeis not recognized, or thereferencebreaks the rule above. The body carries amessagenaming the problem.404 Not Found- Theownerdoes not exist or may not be reached. Thereasonisnot_found.409 Conflict- The hold was not set. The body carries amessageand areason:hold_exists- A hold with this reference already exists for the user.operation_in_progress- Another evidence or hold operation for the user is active, or was interrupted and requires recovery. Retry once it finishes.
Example request:
curl -k -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
"https://localhost:8080/api/holds" \
-d '{"reference":"LIT-2026-001","scopeType":"document_chain","scopeValue":"doc-abc123","reason":"Preserve pending resolution"}'
Hold object
Set, list, and get return holds with these fields:
| Field | Description |
|---|---|
reference |
The hold identifier. |
scopeType |
document_chain or user. |
scopeValue |
The document id, or for a user hold the owner's username. |
reason |
The reason given when the hold was set, or null. |
setAt |
When the hold was set (ISO 8601, UTC). |
owner |
The owner's username. Present only in a listing with all_users=true. |
{
"reference": "LIT-2026-001",
"scopeType": "document_chain",
"scopeValue": "doc-abc123",
"reason": "Preserve pending resolution",
"setAt": "2026-06-08T14:11:33.000Z"
}
List Holds
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/holds |
List active holds, most recently set first. |
Query Parameters
owner(optional, admin only) - Username of another user whose holds to list.offset(optional, default0) - Number of holds to skip.limit(optional, default25, max100) - Maximum number of holds to return.q(optional) - Only holds whose reference containsq, ignoring case.sort(optional, defaultset) -set, newest first by default, orreference. Withorder(ascordesc); see Listings.all_users(optional, defaultfalse) - List every user's holds instead of the caller's; each hold then also has anownerfield with its owner's username. Requires an administrator andADMIN_CROSS_USER_ACCESS_ENABLED=true(disabled by default), asownerdoes; otherwise it returns404 Not Found. Cannot be combined withowner.
Responses
200 OK- The holds on the requested page inholds, each a hold object, andtotal, how many there are across every page.400 Bad Request- Bothownerandall_userswere given, orsortororderis not one this listing accepts.
Example request:
curl -k -H "Authorization: Bearer <token>" "https://localhost:8080/api/holds?offset=0&limit=25"
Get a Hold
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/holds/{reference} |
Retrieve a single hold by its reference. |
Query Parameters
owner(optional, admin only) - Username of another user whose hold to retrieve.
Responses
200 OK- The hold details.404 Not Found- No hold with that reference exists for the user.
Example request:
curl -k -H "Authorization: Bearer <token>" "https://localhost:8080/api/holds/LIT-2026-001"
Release a Hold
| Method | Endpoint | Description |
|---|---|---|
DELETE |
/api/holds/{reference} |
Release (delete) the named hold. |
Once released, evidence previously covered by this hold becomes eligible for deletion if no other hold remains. Only the user who set the hold, or an administrator, can release it, so a user cannot release a hold an administrator placed on their evidence. Releasing a hold does not delete any ledger data. Every release is recorded in the audit log as legal_hold_released.
Query Parameters
owner(optional, admin only) - Username of another user whose hold to release.
Responses
204 No Content- The hold was released.403 Forbidden- The caller neither set the hold nor is an administrator. Thereasonisadmin_required.404 Not Found- No hold with that reference exists for the user. Anownerthat does not exist or may not be reached gets the samenot_foundbody.409 Conflict- The hold was not released because another evidence or hold operation for the user is active or requires recovery. The body carries amessageand thereasonoperation_in_progress.
Example request:
curl -k -X DELETE -H "Authorization: Bearer <token>" "https://localhost:8080/api/holds/LIT-2026-001"
Example 409 response:
{
"message": "An evidence or legal-hold operation is active or requires recovery for this owner.",
"reason": "operation_in_progress"
}
Blocked Deletions
While any hold covers the target evidence, deletion endpoints return 423 Locked and list the blocking hold references in the response body. The attempt is recorded in the audit log as legal_hold_blocked_deletion. See Legal Holds for which operations are checked.