Contents

Retention and storage

How long each record is kept, where it lives, and erasure requests.

The FolioClass object

Attributes

  • id required Type: string
  • name required Type: string
  • description optional Type: string
  • period required Type: any

    For example {"unit": "years", "n": 7}, or {"unit": "no_end"}.

  • start required Type: "filing" "event" "supplied"
  • lock required Type: "retained" "locked"
  • profile required Type: "active" "reference" "archive" "deep_archive"
  • end_action required Type: any

    For example {"action": "delete_after_notice", "notice_days": 30} or {"action": "ask_first"}.

  • version required Type: integer

FolioClass on its own page

List retention classes

GET /folio/classes

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

Every class at its current version. A new organisation has one, Standard (7 years, Retained).

Returns

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

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

Create a class or a new version of one

POST /folio/classes

Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks

Classes are versioned; every record keeps the version it was filed under, so a shorter period applies to new filings only. A change, so a reason is required. Locked classes need the Locked add-on. A version that protects less needs a second person: Locked to Retained, "ask first" to automatic deletion, a shorter notice, a colder storage profile, or (for the default class) a shorter period. The answer is then 202 with the approval, and the version is written when a different person approves (POST /folio/approvals/{id}/approve).

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Request body

FolioClassRequest

  • id required Type: string

    a-z, 0-9, hyphen and underscore, up to 63 characters.

  • name required Type: string
  • description optional Type: string
  • period required Type: any
  • start required Type: "filing" "event" "supplied"
  • lock required Type: "retained" "locked"
  • profile required Type: "active" "reference" "archive" "deep_archive"
  • end_action required Type: any
  • reason_code required Type: string
  • reason_text optional Type: string null

Returns

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

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

Preview applying a class to existing records

GET /folio/classes/{id}/extension

Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks

What applying the class's latest version to its older records would change.

Parameters

  • id pathrequired Type: string

Returns

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

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

Ask to apply a class to existing records

POST /folio/classes/{id}/extend

Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks

Records only ever move later. Nothing changes until a different person approves with POST /folio/approvals/{id}/approve; the request records the preview.

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

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

Assess an erasure request

GET /folio/erasure

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

Lists the subject's records and, for each, whether it could be erased and why not: due (retention has ended), retained (inside a Retained class), locked (inside a Locked class: never erasable early) or held (under a legal hold, with the hold's reason). Ledgered as an audited read.

Parameters

  • subject queryrequired Type: string

Returns

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

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

Ask to erase a subject's records

POST /folio/erasure

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

Erasure is not absolute: retention required by law takes precedence. A record under a legal hold or in a Locked class inside its retention is never erased. Records past their retention are erased; records inside a Retained class's retention only when include_retained is set, which makes the two approvers' decision explicit. Nothing is erased until a different person approves with POST /folio/approvals/{id}/approve; the completed approval carries a result report naming what was erased and what was kept and why. 409 when nothing can be erased now.

Parameters

  • Idempotency-Key headerrequired Type: string

    1–255 printable ASCII characters, kept 24 hours.

Request body

FolioErasureRequest

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

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

Storage profiles

GET /folio/profiles

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

Each storage profile's moves to colder storage (at fixed ages, the same for every organisation), the size and retention below which a record stays warmer, the storage price, and the restore price, speeds and waits. Amounts are null until prices are set.

Returns

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

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

Where folio's data lives

GET /folio/residency

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

The organisation's residency report, exportable for its auditors: its region and stamp, where each class of data lives, the keys and their regions, the flows that may leave the region (only hashes to the time-stamp authority, account metadata to the control plane, and counts and states to operations), each outbound service and whether it is pinned to the region, the personnel jurisdictions, the residency changes (organisation moves), recent denied attempts and when the controls were last tested. Facts only the infrastructure can attest are null until it does, and the report then says complete: false. An organisation whose residency is not this stamp's region is refused on every folio route (403). Ledgered as residency.reported.

Returns

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

r = requests.get(
    "https://api.unforged.sh/folio/residency",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /folio/residency · Node.js
const res = await fetch('https://api.unforged.sh/folio/residency', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());
Response 200
{
  "home_region": "uk-london",
  "cloud_region": "eu-west-2",
  "stamp": "shared",
  "complete": false,
  "data": [
    {
      "class": "documents and their receipts",
      "region": "uk-london",
      "enforced_by": [
        "service"
      ]
    }
  ],
  "keys": [
    {
      "purpose": "the organisation's folio key (documents, second copy, derived data)",
      "region": null,
      "multi_region": null,
      "enforced_by": "infrastructure"
    }
  ],
  "permitted_flows": [
    {
      "what": "hashes only (never content)",
      "to": "the time-stamp authority (uk)"
    }
  ],
  "timestamp_jurisdiction": "uk",
  "outbound": [
    {
      "client": "document store",
      "carries": "documents, attachments, queued files and exports",
      "location": "region",
      "region": "uk-london",
      "pinned": true
    }
  ],
  "personnel_jurisdictions": [
    "GB"
  ],
  "residency_changes": [],
  "infrastructure": {
    "account": null,
    "region_lock": null,
    "bucket_region": null,
    "second_copy_region": null,
    "key_region": null,
    "multi_region_keys": null,
    "cross_region_replication": null,
    "egress_allow_list": null,
    "recent_denials": [],
    "last_tested_ms": null
  },
  "denied_attempts": [],
  "generated_at_ms": 1790000000000
}