Retention and storage
How long each record is kept, where it lives, and erasure requests.
The FolioClass object
Attributes
idrequired Type:stringnamerequired Type:stringdescriptionoptional Type:stringperiodrequired Type:anyFor example {"unit": "years", "n": 7}, or {"unit": "no_end"}.
startrequired Type:"filing""event""supplied"lockrequired Type:"retained""locked"profilerequired Type:"active""reference""archive""deep_archive"end_actionrequired Type:anyFor example {"action": "delete_after_notice", "notice_days": 30} or {"action": "ask_first"}.
versionrequired Type:integer
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
200The classes400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/classes" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" 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()) 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-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
Request body
idrequired Type:stringa-z, 0-9, hyphen and underscore, up to 63 characters.
namerequired Type:stringdescriptionoptional Type:stringperiodrequired Type:anystartrequired Type:"filing""event""supplied"lockrequired Type:"retained""locked"profilerequired Type:"active""reference""archive""deep_archive"end_actionrequired Type:anyreason_coderequired Type:stringreason_textoptional Type:stringnull
Returns
201Type:FolioClassSaved202Type:FolioApprovalWaiting for a second person400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/classes" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/classes",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) 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
idpathrequired Type:string
Returns
200Type:FolioExtensionPreviewThe preview400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/classes/{id}/extension" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" 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()) 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-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
idpathrequired Type:string
Request body
coderequired Type:stringtextoptional Type:stringnull
Returns
201Type:FolioApprovalThe approval request400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/classes/{id}/extend" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/classes/{id}/extend",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) 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
subjectqueryrequired Type:string
Returns
200Type:FolioErasureAssessmentThe assessment400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/erasure?subject=<subject>" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" 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()) 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-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
Request body
subjectrequired Type:stringinclude_retainedoptional Type:booleancoderequired Type:stringtextoptional Type:stringnull
Returns
201Type:FolioApprovalThe approval request400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found409Type:Problemconflict,idempotency_conflict,idempotency_in_progressoridempotency_not_replayable413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/erasure" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{}' import requests
r = requests.post(
"https://api.unforged.sh/folio/erasure",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) 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
200Type:FolioProfilesThe profiles400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/profiles" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" 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()) 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
200Type:FolioResidencyThe report400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized403Type:Problempermission_denied,org_suspended, oraddress_not_allowed(your organisation only accepts requests from the addresses it has listed)404Type:Problemnot_found421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/residency" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" 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()) 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()); {
"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
}