Contents

Records

File documents, read them back, and keep them warm when you need them often.

The FolioRecord object

Attributes

  • id required Type: string
  • folder required Type: string
  • name required Type: string
  • size required Type: integer
  • sha256 required Type: string
  • class_id required Type: string
  • class_version optional Type: integer
  • lock required Type: string

    The retention lock: retained or locked.

  • earliest_delete optional Type: string null
  • state required Type: string
  • matter optional Type: string null
  • subject optional Type: string null
  • metadata optional Type: any
  • receipt optional Type: any
  • filed_by optional Type: string
  • filed_at_ms optional Type: integer null
  • attachments optional Type: object[]

    Evidence filed with the document, each {kind, sha256, body}; a Nib result is nib_result.

  • nib_verdict optional Type: string

    The Nib decision filed with the record, or no verdict recorded.

  • storage optional Type: FolioStorage null

    Where it is kept (UNF-132); null when that could not be worked out.

FolioRecord on its own page

The FolioReceipt object

The receipt for a filed document.

Attributes

  • record_id required Type: string
  • sha256 required Type: string
  • size optional Type: integer
  • class_id optional Type: string
  • earliest_delete optional Type: string null
  • lock optional Type: string

FolioReceipt on its own page

List records

GET /folio/records

Needs folio:records:read: See filed records, folders and metadata

Listing needs no reason but is audited; the ledger records what was listed and which records were shown.

Parameters

  • folder queryoptional Type: string
  • recursive queryoptional Type: boolean
  • after queryoptional Type: string

    The previous page's next_after.

  • limit queryoptional Type: integer

Returns

  • 200 Type: FolioRecordPage
    A page of records
  • 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/records · curl
curl "https://api.unforged.sh/folio/records" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/records · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/records",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/records · Node.js
const res = await fetch('https://api.unforged.sh/folio/records', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

File a document

POST /folio/records

Needs folio:records:write: File records, create folders, extend retention

The request body is the file (at most 50 MB here; for a larger file, up to 2 GiB, use POST /folio/uploads, or the S3 gateway). Filing needs no reason. The file is scanned for malware first: an infected file is refused (400, ledgered, nothing stored) and while no scanner can answer filing is refused (503 not_ready). The response is the receipt: the document's SHA-256, its retention class and lock, and the ledger position, returned once the object is locked. Folder and ancestors are created; the folder's matter and subject are inherited. A mistaken filing cannot be edited or replaced: file the correct document separately.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • folder queryoptional Type: string
  • name queryrequired Type: string
  • class queryoptional Type: string

    The retention class; the organisation's default when omitted.

  • matter queryoptional Type: string
  • subject queryoptional Type: string
  • source_system queryoptional Type: string

    An import's origin (kept as provenance and ledgered as record.imported).

  • source_ref queryoptional Type: string
  • original_date queryoptional Type: string

    The original created or event date; starts retention for classes that count from an event.

Returns

  • 201 Type: FolioReceipt
    Filed
  • 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
  • 503 Type: Problem
    busy, not_ready, org_moving or storage_provisioning (your organisation's storage is still being set up); retry after Retry-After
  • default Type: Problem
    internal
POST /folio/records · curl
curl -X POST "https://api.unforged.sh/folio/records?name=<name>" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
POST /folio/records · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/records?name=<name>",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
POST /folio/records · Node.js
const res = await fetch('https://api.unforged.sh/folio/records?name=<name>', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Open a record

GET /folio/records/{id}

Needs folio:records:read: See filed records, folders and metadata

The record's details and receipt. A view, so a reason is required where the organisation setting or a protected folder says so.

Parameters

  • id pathrequired Type: string
  • Unforged-Reason-Id headeroptional Type: string

    An open reason (POST /folio/reasons), when the organisation or the folder requires one for this view.

  • Unforged-Reason-Code headeroptional Type: string

    Instead of an open reason: a reason code for this one call (one of your organisation's codes). It is opened for exactly the records the call reads and ledgered like any reason.

  • Unforged-Reason headeroptional Type: string

    The reason's text, with Unforged-Reason-Code (plain ASCII).

Returns

  • 200 Type: FolioRecord
    The record
  • 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
  • 428 Type: Problem
    reason_required
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
GET /folio/records/{id} · curl
curl "https://api.unforged.sh/folio/records/{id}" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/records/{id} · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/records/{id}",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/records/{id} · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Download a document

GET /folio/records/{id}/content

Needs folio:documents:read: Open and download documents (a reason is recorded)

The ledger entry is committed before any byte is returned. The stored bytes are checked against the receipt's hash on every whole read; a mismatch is an integrity alert and the transfer is cut short. Send Range to read part of a document (one range, at most 64 MiB a time): the answer is 206 with Content-Range, and each such request is a view like any other. A range that starts past the end is 416, and nothing is ledgered or counted. Every read counts the bytes it sends toward the download allowance.

Parameters

  • id pathrequired Type: string
  • Unforged-Reason-Id headeroptional Type: string

    An open reason (POST /folio/reasons), when the organisation or the folder requires one for this view.

  • Unforged-Reason-Code headeroptional Type: string

    Instead of an open reason: a reason code for this one call (one of your organisation's codes). It is opened for exactly the records the call reads and ledgered like any reason.

  • Unforged-Reason headeroptional Type: string

    The reason's text, with Unforged-Reason-Code (plain ASCII).

  • Range headeroptional Type: string

    One range of bytes: bytes=<first>-<last>, bytes=<first>- or bytes=-<count> (the last bytes). At most 64 MiB is sent at a time, cut from the end; Content-Range says what was sent. Any other form, or several ranges, is answered with the whole document.

  • If-Range headeroptional Type: string

    The ETag of an earlier answer. Anything else, such as a date, is answered with the whole document.

Returns

  • 200
    The document
  • 206
    The part of the document a Range asked for
  • 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
  • 416 Type: Problem
    invalid_request
  • 421 Type: Problem
    wrong_region
  • 428 Type: Problem
    reason_required
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
GET /folio/records/{id}/content · curl
curl "https://api.unforged.sh/folio/records/{id}/content" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/records/{id}/content · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/records/{id}/content",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/records/{id}/content · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/content', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Extend a record's retention

POST /folio/records/{id}/retention

Needs folio:records:write: File records, create folders, extend retention

Retention only moves later. The object store's lock is extended first, then the record and the ledger.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

Request body

FolioRetentionRequest

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

Returns

  • 200
    Extended
  • 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/records/{id}/retention · curl
curl -X POST "https://api.unforged.sh/folio/records/{id}/retention" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{}'
POST /folio/records/{id}/retention · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/records/{id}/retention",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={},
)
print(r.json())
POST /folio/records/{id}/retention · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/retention', {
  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 delete a record before its retention ends

POST /folio/records/{id}/early-delete

Needs folio:records:delete: Request disposal and early deletion

Only Retained records; a Locked record has no such path (409). Nothing is deleted until a different person approves with POST /folio/approvals/{id}/approve; a record under a legal hold cannot be deleted until the hold is released. The disposition certificate says the deletion was early and names both people.

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/records/{id}/early-delete · curl
curl -X POST "https://api.unforged.sh/folio/records/{id}/early-delete" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{}'
POST /folio/records/{id}/early-delete · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/records/{id}/early-delete",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={},
)
print(r.json())
POST /folio/records/{id}/early-delete · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/early-delete', {
  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());

Move a record to another folder

POST /folio/records/{id}/move

Needs folio:records:write: File records, create folders, extend retention

A change, so a reason is required; ledgered with the folder before and after. The stored file, its hashes, class and retention never change. Moving a record out of a protected folder answers 202 with the approval a different person must give (POST /folio/approvals/{id}/approve, which then also needs folio:records:write). 409 when the record is under a hold or the folder already has a file of that name.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

Request body

FolioFolderRequest

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

Returns

  • 200 Type: FolioMove
    Moved
  • 202 Type: FolioApproval
    Waiting for a second person
  • 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/records/{id}/move · curl
curl -X POST "https://api.unforged.sh/folio/records/{id}/move" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{}'
POST /folio/records/{id}/move · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/records/{id}/move",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={},
)
print(r.json())
POST /folio/records/{id}/move · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/move', {
  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());

Queue a document for filing

POST /folio/filings

Needs folio:records:write: File records, create folders, extend retention

The request body is the file. For bulk and import callers: the file is checked, held in folio's staging area and filed by a background job through the same pipeline as POST /folio/records (the same scan, rules, ledger and billing). Follow it with GET /folio/filings/{id}. No reason is needed. A file over 50 MB goes through POST /folio/uploads instead.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • folder queryoptional Type: string
  • name queryrequired Type: string
  • class queryoptional Type: string

    The retention class; the organisation's default when omitted.

  • matter queryoptional Type: string
  • subject queryoptional Type: string
  • source_system queryoptional Type: string

    An import's origin (kept as provenance and ledgered as record.imported).

  • source_ref queryoptional Type: string
  • original_date queryoptional Type: string

    The original created or event date; starts retention for classes that count from an event.

Returns

  • 202 Type: FolioQueuedFiling
    Queued
  • 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/filings · curl
curl -X POST "https://api.unforged.sh/folio/filings?name=<name>" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
POST /folio/filings · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/filings?name=<name>",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
POST /folio/filings · Node.js
const res = await fetch('https://api.unforged.sh/folio/filings?name=<name>', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());
Response 202
{
  "id": "fil_01J9X3M8V7K2Q4R5S6T7U8V9W0",
  "state": "queued",
  "folder": "import",
  "name": "letter.pdf",
  "size": 18234,
  "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "record_id": null,
  "error": null,
  "created_at_ms": 1790000000000,
  "finished_at_ms": null
}

A queued filing

GET /folio/filings/{id}

Needs folio:records:read: See filed records, folders and metadata

queued, then filed with its record_id, refused with why (a bad name, a clash, a file the malware scan refused), or failed after the last retry.

Parameters

  • id pathrequired Type: string

    A record, queued filing or export id.

Returns

  • 200 Type: FolioQueuedFiling
    The queued filing
  • 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/filings/{id} · curl
curl "https://api.unforged.sh/folio/filings/{id}" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/filings/{id} · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/filings/{id}",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/filings/{id} · Node.js
const res = await fetch('https://api.unforged.sh/folio/filings/{id}', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Your open large uploads

GET /folio/uploads

Needs folio:records:read: See filed records, folders and metadata

The caller's own large uploads that are still uploading or checking, newest first (at most 100), and intake_origin, the origin every part's URL names. Choosing the same file again carries on where an upload stopped.

Returns

  • 200 Type: FolioUploadList
    The open uploads
  • 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/uploads · curl
curl "https://api.unforged.sh/folio/uploads" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/uploads · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/uploads",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/uploads · Node.js
const res = await fetch('https://api.unforged.sh/folio/uploads', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Start a large upload

POST /folio/uploads

Needs folio:records:write: File records, create folders, extend retention

For a file over 50 MB, up to 2 GiB (2147483648 bytes). The file goes in parts straight to your organisation's own intake storage, then it is scanned whole and filed through the same pipeline as POST /folio/records. Give the filing's options and the file's size; the answer says how big each part is and how many there are. Then sign parts with POST /folio/uploads/{id}/parts, PUT each one to its URL, finish with POST /folio/uploads/{id}/complete and follow GET /folio/uploads/{id} to the record. 413 too_large over 2 GiB; 409 when the name is taken in that folder, the same file is already on its way, or your organisation has too many large uploads open; 503 org_moving while your organisation is moving region. Open to API keys with folio:records:write.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Request body

FolioUploadStart

  • folder optional Type: string
  • name required Type: string
  • class optional Type: string

    The retention class; the organisation's default when omitted.

  • matter optional Type: string
  • subject optional Type: string
  • source_system optional Type: string
  • source_ref optional Type: string
  • original_date optional Type: string
  • size required Type: integer

Returns

  • 201 Type: FolioUploadStarted
    Started
  • 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
  • 503 Type: Problem
    busy, not_ready, org_moving or storage_provisioning (your organisation's storage is still being set up); retry after Retry-After
  • default Type: Problem
    internal
POST /folio/uploads · curl
curl -X POST "https://api.unforged.sh/folio/uploads" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{
  "folder": "scans/2026",
  "name": "deeds-bundle.pdf",
  "size": 734003200
}'
POST /folio/uploads · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/uploads",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={
        "folder": "scans/2026",
        "name": "deeds-bundle.pdf",
        "size": 734003200
    },
)
print(r.json())
POST /folio/uploads · Node.js
const res = await fetch('https://api.unforged.sh/folio/uploads', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "folder": "scans/2026",
    "name": "deeds-bundle.pdf",
    "size": 734003200
  }),
});
console.log(await res.json());
Response 201
{
  "id": "lup_01J9X3M8V7K2Q4R5S6T7U8V9W0",
  "state": "uploading",
  "size": 734003200,
  "part_size": 33554432,
  "part_count": 22,
  "max_parts_in_flight": 4
}

A large upload

GET /folio/uploads/{id}

Needs folio:records:read: See filed records, folders and metadata

uploading (with the parts that have arrived, each with its SHA-256, so carrying on sends only what is missing or different), then checking once it is complete, then filed with its record_id, refused with why (the malware scan, a file the scanner could not check whole, a clash), failed after the last retry, or aborted. Only the person or key that started it sees it; anyone else gets 404.

Parameters

  • id pathrequired Type: string

Returns

  • 200 Type: FolioUpload
    The upload
  • 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/uploads/{id} · curl
curl "https://api.unforged.sh/folio/uploads/{id}" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/uploads/{id} · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/uploads/{id}",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/uploads/{id} · Node.js
const res = await fetch('https://api.unforged.sh/folio/uploads/{id}', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());
Response 200
{
  "id": "lup_01J9X3M8V7K2Q4R5S6T7U8V9W0",
  "state": "checking",
  "folder": "scans/2026",
  "name": "deeds-bundle.pdf",
  "size": 734003200,
  "part_size": 33554432,
  "part_count": 22,
  "parts": [],
  "bytes_received": 734003200,
  "record_id": null,
  "error": null,
  "created_at_ms": 1790000000000,
  "updated_at_ms": 1790000420000
}

Stop a large upload

DELETE /folio/uploads/{id}

Needs folio:records:write: File records, create folders, extend retention

Stops an upload that is still uploading; whatever was sent is thrown away. One that has already finished answers as it stands; one that is checking can't be stopped (409).

Parameters

  • id pathrequired Type: string

Returns

  • 200 Type: FolioUpload
    The upload, aborted
  • 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
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
DELETE /folio/uploads/{id} · curl
curl -X DELETE "https://api.unforged.sh/folio/uploads/{id}" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
DELETE /folio/uploads/{id} · Python
import requests

r = requests.delete(
    "https://api.unforged.sh/folio/uploads/{id}",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
DELETE /folio/uploads/{id} · Node.js
const res = await fetch('https://api.unforged.sh/folio/uploads/{id}', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Sign parts of a large upload

POST /folio/uploads/{id}/parts

Needs folio:records:write: File records, create folders, extend retention

Up to 8 parts at a time. Each part is named by its number (from 1), its size (every part but the last is exactly part_size bytes) and the base64 SHA-256 of its bytes. Send each part with the method, to the url, with exactly the headers given, before expires_at_ms (15 minutes); a part whose bytes don't match its SHA-256 is refused by storage and can be signed and sent again.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

Request body

FolioUploadSignRequest

  • parts required Type: object[]

Returns

  • 200 Type: FolioUploadSigned
    The signed parts
  • 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
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/uploads/{id}/parts · curl
curl -X POST "https://api.unforged.sh/folio/uploads/{id}/parts" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{
  "parts": [
    {
      "number": 1,
      "size": 33554432,
      "sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
    }
  ]
}'
POST /folio/uploads/{id}/parts · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/uploads/{id}/parts",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={
        "parts": [
            {
                "number": 1,
                "size": 33554432,
                "sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
            }
        ]
    },
)
print(r.json())
POST /folio/uploads/{id}/parts · Node.js
const res = await fetch('https://api.unforged.sh/folio/uploads/{id}/parts', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "parts": [
      {
        "number": 1,
        "size": 33554432,
        "sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
      }
    ]
  }),
});
console.log(await res.json());
Response 200
{
  "parts": [
    {
      "number": 1,
      "method": "PUT",
      "url": "https://unf-prod-org-01j9x3m8v7-folio-intake.s3.eu-west-2.amazonaws.com/u/lup_01J9X3M8V7K2Q4R5S6T7U8V9W0?partNumber=1&uploadId=example&X-Amz-Algorithm=AWS4-HMAC-SHA256",
      "headers": {
        "x-amz-checksum-sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
      },
      "expires_at_ms": 1790000900000
    }
  ]
}

Finish a large upload

POST /folio/uploads/{id}/complete

Needs folio:records:write: File records, create folders, extend retention

List every part once, with the SHA-256 you signed it for. The parts storage holds are checked against that list and the file's size, then the file is put together and queued to be scanned and filed: 202 with the upload checking. Calling it again answers the upload as it stands. 400 when a part is missing or isn't the one that was signed: send that part again and finish again.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

Request body

FolioUploadCompleteRequest

  • parts required Type: object[]

Returns

  • 202 Type: FolioUpload
    Complete, and queued for filing
  • 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
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/uploads/{id}/complete · curl
curl -X POST "https://api.unforged.sh/folio/uploads/{id}/complete" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{}'
POST /folio/uploads/{id}/complete · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/uploads/{id}/complete",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={},
)
print(r.json())
POST /folio/uploads/{id}/complete · Node.js
const res = await fetch('https://api.unforged.sh/folio/uploads/{id}/complete', {
  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());
Response 202
{
  "id": "lup_01J9X3M8V7K2Q4R5S6T7U8V9W0",
  "state": "checking",
  "folder": "scans/2026",
  "name": "deeds-bundle.pdf",
  "size": 734003200,
  "part_size": 33554432,
  "part_count": 22,
  "parts": [],
  "bytes_received": 734003200,
  "record_id": null,
  "error": null,
  "created_at_ms": 1790000000000,
  "updated_at_ms": 1790000420000
}

File a Nib check's document with its result

POST /folio/records/from-check

Needs folio:records:write: File records, create folders, extend retention

Copies the checked document and its result into folio: the result is attached to the record as hashed evidence and the record's provenance names the check. The check and its result are unchanged. Also needs nib:checks:read. 409 when Nib no longer keeps the document.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Request body

FolioFromCheck

  • job_id required Type: string
  • item_index required Type: integer
  • folder optional Type: string
  • name optional Type: string

    The file name in folio; the checked file's own name when omitted.

  • class optional Type: string
  • matter optional Type: string
  • subject optional Type: string

Returns

  • 201 Type: FolioReceipt
    Filed
  • 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
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/records/from-check · curl
curl -X POST "https://api.unforged.sh/folio/records/from-check" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{
  "job_id": "job-2291",
  "item_index": 0,
  "folder": "cases/2291"
}'
POST /folio/records/from-check · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/records/from-check",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={
        "job_id": "job-2291",
        "item_index": 0,
        "folder": "cases/2291"
    },
)
print(r.json())
POST /folio/records/from-check · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/from-check', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "job_id": "job-2291",
    "item_index": 0,
    "folder": "cases/2291"
  }),
});
console.log(await res.json());

A record's custody report

GET /folio/records/{id}/custody

Needs folio:exports:create: Create exports, custody and integrity reports

Every ledger event about the record from filing to date, in order, with actors, reasons, times and each entry's place in the hash chain, plus a fresh integrity check. Also needs folio:audit:read. An audited read, ledgered as record.custody_reported; no reason.

Parameters

  • id pathrequired Type: string

    A record, queued filing or export id.

Returns

  • 200 Type: FolioCustody
    The custody report
  • 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/records/{id}/custody · curl
curl "https://api.unforged.sh/folio/records/{id}/custody" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/records/{id}/custody · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/records/{id}/custody",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/records/{id}/custody · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/custody', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Check a record's integrity now

GET /folio/records/{id}/integrity

Needs folio:exports:create: Create exports, custody and integrity reports

Reads the stored bytes and compares their SHA-256 with the receipt (ok, mismatch or unreadable). A mismatch raises folio.integrity.alert. A clean result means the bytes are the ones filed; it does not say the document is genuine.

Parameters

  • id pathrequired Type: string

    A record, queued filing or export id.

Returns

  • 200 Type: FolioIntegrityResult
    The result
  • 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/records/{id}/integrity · curl
curl "https://api.unforged.sh/folio/records/{id}/integrity" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/records/{id}/integrity · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/records/{id}/integrity",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/records/{id}/integrity · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/integrity', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Where a record is kept, and its restore

GET /folio/records/{id}/restore

Needs folio:records:read: See filed records, folders and metadata

The record's storage (its tier, the storage class it is in, whether it needs a restore before it can be opened, the pin keeping it out of colder storage) and its open restore, or the latest one. Not a view: nothing of the document is shown.

Parameters

  • id pathrequired Type: string

    A record, queued filing or export id.

Returns

  • 200
    Where it is kept
  • 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/records/{id}/restore · curl
curl "https://api.unforged.sh/folio/records/{id}/restore" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/records/{id}/restore · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/records/{id}/restore",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/records/{id}/restore · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/restore', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Restore an archived record

POST /folio/records/{id}/restore

Needs folio:documents:read: Open and download documents (a reason is recorded)

Asks for a readable copy of a record that has moved to Glacier Flexible Retrieval or Deep Archive, kept for days (7 by default, 1 to 30). standard takes 3 to 5 hours from Flexible Retrieval and 9 to 12 from Deep Archive; bulk takes 5 to 12 and 24 to 48. The price is the same at either speed. Authorised and ledgered as a view (record.restore_requested), so the organisation's reason rules apply, and metered once. 202 when it is asked for now; 200 with the open restore when there already is one (it is not asked for or charged again); 409 not_archived when the record can be opened as it is. When it is ready the folio.restore.ready webhook is sent, and an email goes to notify_email when one is given.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

  • id pathrequired Type: string

    A record, queued filing or export id.

  • Unforged-Reason-Id headeroptional Type: string

    An open reason (POST /folio/reasons), when the organisation or the folder requires one for this view.

  • Unforged-Reason-Code headeroptional Type: string

    Instead of an open reason: a reason code for this one call (one of your organisation's codes). It is opened for exactly the records the call reads and ledgered like any reason.

  • Unforged-Reason headeroptional Type: string

    The reason's text, with Unforged-Reason-Code (plain ASCII).

Request body

FolioRestoreRequest

  • speed optional Type: "standard" "bulk"
  • days optional Type: integer

    How long the restored copy can be read.

  • notify_email optional Type: string null

    Who to email when it is ready.

Returns

  • 200 Type: FolioRestoreResult
    A restore was already open for this record; it is returned as it is
  • 202 Type: FolioRestoreResult
    Asked for
  • 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
  • 428 Type: Problem
    reason_required
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /folio/records/{id}/restore · curl
curl -X POST "https://api.unforged.sh/folio/records/{id}/restore" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{}'
POST /folio/records/{id}/restore · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/records/{id}/restore",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={},
)
print(r.json())
POST /folio/records/{id}/restore · Node.js
const res = await fetch('https://api.unforged.sh/folio/records/{id}/restore', {
  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 restores

GET /folio/restores

Needs folio:records:read: See filed records, folders and metadata

The organisation's restores, newest first. open=true keeps those asked for, running or ready.

Parameters

  • open queryoptional Type: boolean
  • before queryoptional Type: string

    The next of the previous page.

  • limit queryoptional Type: integer

Returns

  • 200
    The restores
  • 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/restores · curl
curl "https://api.unforged.sh/folio/restores" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/restores · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/restores",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/restores · Node.js
const res = await fetch('https://api.unforged.sh/folio/restores', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

List pins

GET /folio/pins

Needs folio:records:read: See filed records, folders and metadata

Parameters

  • include_ended queryoptional Type: boolean

Returns

  • 200
    The pins, 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/pins · curl
curl "https://api.unforged.sh/folio/pins" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /folio/pins · Python
import requests

r = requests.get(
    "https://api.unforged.sh/folio/pins",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/pins · Node.js
const res = await fetch('https://api.unforged.sh/folio/pins', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Keep a record or folder out of colder storage

POST /folio/pins

Needs folio:records:write: File records, create folders, extend retention

Pins a record, or a folder and everything below it, so it stops moving to colder storage. Anything already in an archive class stays there and still needs a restore to open: a pin stops future moves, it does not bring anything back. Storage is then charged at the rate of the class each document is in. A change, so a reason is required; ledgered (storage.pinned). Pinning what is already pinned answers its active pin.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Request body

FolioPinRequest

  • kind required Type: "record" "folder"
  • value required Type: string

    The record id, or the folder (everything below it is covered too).

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

Returns

  • 201 Type: FolioPin
    Pinned
  • 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/pins · curl
curl -X POST "https://api.unforged.sh/folio/pins" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{}'
POST /folio/pins · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/pins",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={},
)
print(r.json())
POST /folio/pins · Node.js
const res = await fetch('https://api.unforged.sh/folio/pins', {
  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 pin

POST /folio/pins/{id}/end

Needs folio:records:write: File records, create folders, extend retention

The pinned records take their storage profile's moves again, by their age. A change, so a reason is required; ledgered (storage.unpinned). Ending an ended pin answers it as it is.

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: FolioPin
    Ended
  • 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/pins/{id}/end · curl
curl -X POST "https://api.unforged.sh/folio/pins/{id}/end" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{}'
POST /folio/pins/{id}/end · Python
import requests

r = requests.post(
    "https://api.unforged.sh/folio/pins/{id}/end",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={},
)
print(r.json())
POST /folio/pins/{id}/end · Node.js
const res = await fetch('https://api.unforged.sh/folio/pins/{id}/end', {
  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());