Holds, reasons and approvals
Legal holds, the reasons people give for opening records, and two-person approvals.
The FolioHold object
Attributes
idrequired Type:stringscoperequired Type:anyreason_coderequired Type:stringreason_textoptional Type:stringnullplaced_byrequired Type:stringplaced_at_msrequired Type:integerreleased_at_msoptional Type:integernullreleased_byoptional Type:stringnullrecords_coveredoptional Type:integer
The FolioApproval object
Attributes
idrequired Type:stringactionrequired Type:"release_hold""early_delete""apply_class""erase_subject""config_change""unprotect_folder""move_record""class_change""rename_folder"targetoptional Type:anyWhat the request would do (for a configuration change: the configuration before and after, and what it reduces).
requested_byrequired Type:stringrequest_codeoptional Type:stringThe requester's reason code.
request_textoptional Type:stringnullrequested_at_msoptional Type:integerexpires_at_msoptional Type:integerstaterequired Type:"pending""done""refused"approved_byoptional Type:stringnullWho decided it: approved it or, when
refused, refused it.resultoptional Type:anyWhat the approval did, where it reports (an erasure: the records erased, and those kept with the reason).
decided_at_msoptional Type:integernullWhen it was approved or refused.
Open a reason
POST /folio/reasons
Needs folio:records:read: See filed records, folders and metadata
A reason is given once for a context (a folder and its sub-folders, a matter, a subject or a list of records) and covers every associated record until it expires, at most 20 minutes, or is ended. Pass its id as Unforged-Reason-Id.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
Request body
coderequired Type:stringOne of the organisation's reason codes.
textoptional Type:stringnullmatteroptional Type:stringnullcontextrequired Type:objectsecondsoptional Type:integerRequested lifetime; capped by the organisation and at 20 minutes.
Returns
201Type:FolioReasonOpened400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/reasons" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/reasons",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/reasons', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
console.log(await res.json()); End a reason
DELETE /folio/reasons/{id}
Needs folio:records:read: See filed records, folders and metadata
Parameters
idpathrequired Type:string
Returns
204Ended (ending an unknown or ended reason is not an error)400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X DELETE "https://api.unforged.sh/folio/reasons/{id}" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.delete(
"https://api.unforged.sh/folio/reasons/{id}",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/reasons/{id}', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); List holds
GET /folio/holds
Needs folio:holds:manage: Place and request release of legal holds
Parameters
include_releasedqueryoptional Type:boolean
Returns
200The holds, newest first400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/holds" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/holds",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/holds', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Place a legal hold
POST /folio/holds
Needs folio:holds:manage: Place and request release of legal holds
Suspends disposal of every record in the scope (a record, a folder and everything under it, a matter or a subject) until the hold is released. A change, so a reason is required; every covered record is ledgered. Records filed into the scope later are covered too.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
Request body
scoperequired Type:objectcoderequired Type:stringtextoptional Type:stringnull
Returns
201Type:FolioHoldPlaced400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/holds" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/holds",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/holds', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
console.log(await res.json()); Ask to release a hold
POST /folio/holds/{id}/release
Needs folio:holds:manage: Place and request release of legal holds
Nothing is released until a different person approves (POST /folio/approvals/{id}/approve).
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
idpathrequired Type:string
Request body
coderequired Type:stringtextoptional Type:stringnull
Returns
201Type:FolioApprovalThe approval request400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/holds/{id}/release" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/holds/{id}/release",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/holds/{id}/release', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
console.log(await res.json()); Approve a request
POST /folio/approvals/{id}/approve
Needs folio:holds:approve: Approve hold releases and early deletion (second person)
The approver must be a different person from the requester (409 otherwise), with a reason of their own. The request expires after seven days and is used once.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
idpathrequired Type:string
Request body
coderequired Type:stringtextoptional Type:stringnull
Returns
200Type:FolioApprovalApproved and carried out400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/approvals/{id}/approve" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/approvals/{id}/approve",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/approvals/{id}/approve', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
console.log(await res.json()); Refuse a request
POST /folio/approvals/{id}/refuse
Needs folio:holds:approve: Approve hold releases and early deletion (second person)
The refuser must be a different person from the requester (409 otherwise), with a reason of their own, and needs what approving it would need. Nothing the request asked for happens; the request is then refused and can never be approved. A requester who changes their mind leaves it to expire.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
idpathrequired Type:string
Request body
coderequired Type:stringtextoptional Type:stringnull
Returns
200Type:FolioApprovalRefused400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/approvals/{id}/refuse" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/approvals/{id}/refuse",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/approvals/{id}/refuse', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
console.log(await res.json()); List regulator grants
GET /folio/grants
Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks
Returns
200The grants, newest first401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/grants" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/grants",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/grants', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Ask for short-lived regulator access
POST /folio/grants
Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks
While access is suspended for non-payment, ask for short-lived access to respond to a regulator: the regulator and its reference, the folders or matters needed, the people who need access, 24, 48 or 72 hours (48 by default) and a declaration. Two members of Unforged staff approve it; they never see a document. Once approved, the named people can read, search and export the records in the named folders, matters or subjects for that time only (listing and search show only those), and every access is recorded with the reason regulatory_request and the reference. Five grants in any rolling 12 months; later ones carry a one-off fee. Allowed while suspended.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
Returns
201Requested; it waits for two approvals400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/grants" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.post(
"https://api.unforged.sh/folio/grants",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/grants', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); List two-person requests, pending or by how they ended
GET /folio/approvals
Needs folio:records:read: See filed records, folders and metadata
Requests newest first: by default those not yet decided and not expired. state picks pending (the default), approved, refused or expired (never decided in time), so a requester can see how their requests ended. whose: mine (requests the caller made), others (requests the caller could approve; listed only for a caller with folio:holds:approve) or all (the default); mine=true is the same as whose=mine. Page with before (the next_before of the previous page).
Parameters
whosequeryoptional Type:"mine""others""all"statequeryoptional Type:"pending""approved""refused""expired"minequeryoptional Type:booleanbeforequeryoptional Type:stringlimitqueryoptional Type:integer
Returns
200Type:FolioApprovalListThe requests in the chosen state (pending unlessstatesays otherwise), filtered bywhoseormine400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/approvals" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/approvals",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/approvals', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());