Exports and links
Take records out with their receipts, or share one through a link.
The FolioExport object
Attributes
idrequired Type:stringkindrequired Type:"records""custody""integrity""ledger"staterequired Type:"queued""running""ready""failed""expired"recordsrequired Type:integersizerequired Type:integernullsha256required Type:stringnullcreated_at_msrequired Type:integerready_at_msrequired Type:integernullexpires_at_msrequired Type:integernull
The FolioLink object
Attributes
idrequired Type:stringrecord_idrequired Type:stringurlrequired Type:stringRelative to the API base URL; shown once.
expires_at_msrequired Type:integer
Request an export
POST /folio/exports
Needs folio:exports:create: Create exports, custody and integrity reports
records bundles each record's document, receipt, record.json, attachments, a hash manifest and (with folio:audit:read) its custody history; custody, integrity and ledger (needs folio:audit:read) are reports. A records export reads documents, so it also needs folio:documents:read, is a view of each one and takes a reason like opening a record (428 without one where the rules require it). The scope (records, a folder, a matter or a subject) is expanded into its records first; a records export whose documents add up to more than 512 MB is refused (400). Built by a background job; folio.export.ready is sent when done. Only the requester can read or download it.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
Unforged-Reason-Idheaderoptional Type:stringAn open reason (
POST /folio/reasons), when the organisation or the folder requires one for this view.Unforged-Reason-Codeheaderoptional Type:stringInstead 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-Reasonheaderoptional Type:stringThe reason's text, with
Unforged-Reason-Code(plain ASCII).
Request body
kindrequired Type:"records""custody""integrity""ledger"recordsoptional Type:string[]folderoptional Type:stringrecursiveoptional Type:booleanmatteroptional Type:stringsubjectoptional Type:string
Returns
202Type:FolioExportRequested400Type: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_replayable421Type:Problemwrong_region428Type:Problemreason_required429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/exports" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{
"kind": "records",
"records": [
"rec_01J9X3M8V7K2Q4R5S6T7U8V9W0"
]
}' import requests
r = requests.post(
"https://api.unforged.sh/folio/exports",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={
"kind": "records",
"records": [
"rec_01J9X3M8V7K2Q4R5S6T7U8V9W0"
]
},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/exports', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({
"kind": "records",
"records": [
"rec_01J9X3M8V7K2Q4R5S6T7U8V9W0"
]
}),
});
console.log(await res.json()); {
"id": "exp_01J9X3M8V7K2Q4R5S6T7U8V9W0",
"kind": "records",
"state": "queued",
"records": 1,
"size": null,
"sha256": null,
"created_at_ms": 1790000000000,
"ready_at_ms": null,
"expires_at_ms": null
} An export
GET /folio/exports/{id}
Needs folio:exports:create: Create exports, custody and integrity reports
queued, running, ready (downloadable until expires_at_ms), failed or expired. Only the person who requested it sees it (anyone else: 404).
Parameters
idpathrequired Type:stringA record, queued filing or export id.
Returns
200Type:FolioExportThe export400Type: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/exports/{id}" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/exports/{id}",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/exports/{id}', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Download an export
GET /folio/exports/{id}/download
Needs folio:exports:create: Create exports, custody and integrity reports
The bundle, a zip with manifest.json, streamed. Only for the person who requested it (anyone else: 404), who must still hold folio:documents:read for a records export and folio:audit:read for a ledger or custody one (403 otherwise). Ledgered (export.downloaded) before any byte is returned. 409 until it is ready, or once it has expired.
Parameters
idpathrequired Type:stringA record, queued filing or export id.
Returns
200The bundle400Type: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_replayable421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl "https://api.unforged.sh/folio/exports/{id}/download" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/exports/{id}/download",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/exports/{id}/download', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Download through a link
GET /folio/links/{token}
No API key needed
No key: the link is the credential. An unknown, tampered or expired link is 404. view=inline shows a PDF, a PNG, JPEG, GIF or WebP image, or plain text in place, with a policy that runs no script and lets only the web app frame it; any other type is still a download. A browser's viewer reads a large document in parts with Range (one range, at most 64 MiB a time): the answer is 206 with Content-Range. A link's range requests are ledgered as one view, when the first of them serves a byte; each whole download is a view of its own. 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
tokenpathrequired Type:stringviewqueryoptional Type:"inline"Rangeheaderoptional Type:stringOne range of bytes:
bytes=<first>-<last>,bytes=<first>-orbytes=-<count>(the last bytes). At most 64 MiB is sent at a time, cut from the end;Content-Rangesays what was sent. Any other form, or several ranges, is answered with the whole document.If-Rangeheaderoptional Type:stringThe
ETagof an earlier answer. Anything else, such as a date, is answered with the whole document.
Returns
curl "https://api.unforged.sh/folio/links/{token}" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/links/{token}",
headers={"Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/links/{token}', {
method: 'GET',
headers: { 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());