Contents

Holds, reasons and approvals

Legal holds, the reasons people give for opening records, and two-person approvals.

The FolioHold object

Attributes

  • id required Type: string
  • scope required Type: any
  • reason_code required Type: string
  • reason_text optional Type: string null
  • placed_by required Type: string
  • placed_at_ms required Type: integer
  • released_at_ms optional Type: integer null
  • released_by optional Type: string null
  • records_covered optional Type: integer

FolioHold on its own page

The FolioApproval object

Attributes

  • id required Type: string
  • action required Type: "release_hold" "early_delete" "apply_class" "erase_subject" "config_change" "unprotect_folder" "move_record" "class_change" "rename_folder"
  • target optional Type: any

    What the request would do (for a configuration change: the configuration before and after, and what it reduces).

  • requested_by required Type: string
  • request_code optional Type: string

    The requester's reason code.

  • request_text optional Type: string null
  • requested_at_ms optional Type: integer
  • expires_at_ms optional Type: integer
  • state required Type: "pending" "done" "refused"
  • approved_by optional Type: string null

    Who decided it: approved it or, when refused, refused it.

  • result optional Type: any

    What the approval did, where it reports (an erasure: the records erased, and those kept with the reason).

  • decided_at_ms optional Type: integer null

    When it was approved or refused.

FolioApproval on its own page

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-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Request body

FolioReasonRequest

  • code required Type: string

    One of the organisation's reason codes.

  • text optional Type: string null
  • matter optional Type: string null
  • context required Type: object
  • seconds optional Type: integer

    Requested lifetime; capped by the organisation and at 20 minutes.

Returns

  • 201 Type: FolioReason
    Opened
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 409 Type: Problem
    conflict, idempotency_conflict, idempotency_in_progress or idempotency_not_replayable
  • 413 Type: Problem
    too_large
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/reasons · curl
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 '{}'
POST /folio/reasons · Python
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())
POST /folio/reasons · Node.js
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

  • id pathrequired Type: string

Returns

  • 204
    Ended (ending an unknown or ended reason is not an error)
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
DELETE /folio/reasons/{id} · curl
curl -X DELETE "https://api.unforged.sh/folio/reasons/{id}" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
DELETE /folio/reasons/{id} · Python
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())
DELETE /folio/reasons/{id} · Node.js
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_released queryoptional Type: boolean

Returns

  • 200
    The holds, newest first
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
GET /folio/holds · curl
curl "https://api.unforged.sh/folio/holds" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/holds · Python
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())
GET /folio/holds · Node.js
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-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Request body

FolioHoldRequest

  • scope required Type: object
  • code required Type: string
  • text optional Type: string null

Returns

  • 201 Type: FolioHold
    Placed
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 409 Type: Problem
    conflict, idempotency_conflict, idempotency_in_progress or idempotency_not_replayable
  • 413 Type: Problem
    too_large
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/holds · curl
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 '{}'
POST /folio/holds · Python
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())
POST /folio/holds · Node.js
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-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

Request body

FolioReasonOnly

  • code required Type: string
  • text optional Type: string null

Returns

  • 201 Type: FolioApproval
    The approval request
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 409 Type: Problem
    conflict, idempotency_conflict, idempotency_in_progress or idempotency_not_replayable
  • 413 Type: Problem
    too_large
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/holds/{id}/release · curl
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 '{}'
POST /folio/holds/{id}/release · Python
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())
POST /folio/holds/{id}/release · Node.js
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-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

Request body

FolioReasonOnly

  • code required Type: string
  • text optional Type: string null

Returns

  • 200 Type: FolioApproval
    Approved and carried out
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 409 Type: Problem
    conflict, idempotency_conflict, idempotency_in_progress or idempotency_not_replayable
  • 413 Type: Problem
    too_large
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/approvals/{id}/approve · curl
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 '{}'
POST /folio/approvals/{id}/approve · Python
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())
POST /folio/approvals/{id}/approve · Node.js
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-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

Request body

FolioReasonOnly

  • code required Type: string
  • text optional Type: string null

Returns

  • 200 Type: FolioApproval
    Refused
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 409 Type: Problem
    conflict, idempotency_conflict, idempotency_in_progress or idempotency_not_replayable
  • 413 Type: Problem
    too_large
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/approvals/{id}/refuse · curl
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 '{}'
POST /folio/approvals/{id}/refuse · Python
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())
POST /folio/approvals/{id}/refuse · Node.js
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

  • 200
    The grants, newest first
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
GET /folio/grants · curl
curl "https://api.unforged.sh/folio/grants" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/grants · Python
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())
GET /folio/grants · Node.js
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-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Returns

  • 201
    Requested; it waits for two approvals
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 409 Type: Problem
    conflict, idempotency_conflict, idempotency_in_progress or idempotency_not_replayable
  • 413 Type: Problem
    too_large
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/grants · curl
curl -X POST "https://api.unforged.sh/folio/grants" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
POST /folio/grants · Python
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())
POST /folio/grants · Node.js
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

  • whose queryoptional Type: "mine" "others" "all"
  • state queryoptional Type: "pending" "approved" "refused" "expired"
  • mine queryoptional Type: boolean
  • before queryoptional Type: string
  • limit queryoptional Type: integer

Returns

  • 200 Type: FolioApprovalList
    The requests in the chosen state (pending unless state says otherwise), filtered by whose or mine
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
GET /folio/approvals · curl
curl "https://api.unforged.sh/folio/approvals" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/approvals · Python
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())
GET /folio/approvals · Node.js
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());