Imports
Bring in whole collections at once, from an upload or your own storage tools.
The FolioImport object
Attributes
idrequired Type:stringreferencerequired Type:stringsource_systemrequired Type:stringstaterequired Type:"open""closed""complete"countsrequired Type:FolioImportCountsbytes_expectedrequired Type:integerbytes_filedrequired Type:integerbytes_to_filerequired Type:integerThe declared bytes of the lines waiting for their file or queued.
percentrequired Type:integercreated_at_msrequired Type:integerclosed_at_msrequired Type:integernullcompleted_at_msrequired Type:integernull
The organisation's imports
GET /folio/imports
Needs folio:records:read: See filed records, folders and metadata
The latest 50 imports, newest first, each with its progress.
Returns
200The imports400Type: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/imports" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/imports",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Start an import, or add lines to it
POST /folio/imports
Needs folio:records:write: File records, create folders, extend retention
A bulk import of an existing archive: send the manifest, then each line's file to POST /folio/imports/{id}/content. The body is a JSON manifest, or a CSV one (Content-Type: text/csv, a header row naming the columns reference, folder, name, sha256, size, class, original_date, event_date, matter, subject, with reference and source_system in the query). The import is named by its reference: the same manifest sent again, or in parts of up to 10,000 lines, adds only the lines it does not have, so an interrupted import is resumed by running it again. The answer carries the estimate of what is left to file, before any file is sent. A line repeated with another hash, or without a usable reference (1 to 200 characters), is listed in rejected and changes nothing. A line that cannot be filed (a bad name, an unknown class, a Locked class without the Locked add-on) is refused with why; a file the organisation has already filed, or an earlier line of the import carries, is a duplicate and is not filed again. Ledgered as import.started.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
referencequeryoptional Type:stringThe import's reference, for a CSV manifest.
source_systemqueryoptional Type:stringThe system the archive comes from, for a CSV manifest.
Request body
referencerequired Type:stringThe import's name, chosen by you: the same reference resumes the same import.
source_systemrequired Type:stringitemsrequired Type:FolioImportManifestLine[]
Returns
200Type:FolioImportAcceptedThe import, and what this call added400Type: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/imports" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{
"reference": "legacy-2026",
"source_system": "LegacyDMS",
"items": [
{
"reference": "dms/1",
"folder": "archive/2020",
"name": "letter.pdf",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"size": 18234,
"original_date": "2020-01-15"
}
]
}' import requests
r = requests.post(
"https://api.unforged.sh/folio/imports",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={
"reference": "legacy-2026",
"source_system": "LegacyDMS",
"items": [
{
"reference": "dms/1",
"folder": "archive/2020",
"name": "letter.pdf",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"size": 18234,
"original_date": "2020-01-15"
}
]
},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({
"reference": "legacy-2026",
"source_system": "LegacyDMS",
"items": [
{
"reference": "dms/1",
"folder": "archive/2020",
"name": "letter.pdf",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"size": 18234,
"original_date": "2020-01-15"
}
]
}),
});
console.log(await res.json()); {
"import": {
"id": "imp_01J9X3M8V7K2Q4R5S6T7U8V9W0",
"reference": "legacy-2026",
"source_system": "LegacyDMS",
"state": "open",
"counts": {
"total": 2,
"awaiting": 2,
"queued": 0,
"filed": 0,
"duplicate": 0,
"refused": 0,
"failed": 0
},
"bytes_expected": 36468,
"bytes_filed": 0,
"bytes_to_file": 36468,
"percent": 0,
"created_at_ms": 1790000000000,
"closed_at_ms": null,
"completed_at_ms": null
},
"added": 2,
"unchanged": 0,
"rejected": [],
"estimate": {
"estimate": {
"files": 2,
"bytes": 36468,
"pages": 2,
"profile": "reference",
"filing_minor": null,
"reading_minor": null,
"storage_month_minor": null,
"first_month_minor": null
},
"priced": false
}
} An import's progress
GET /folio/imports/{id}
Needs folio:records:read: See filed records, folders and metadata
Line counts by state, bytes declared, filed and still to file, the percentage finished, and the estimate of what is left to file (filing, reading at least a page a file, and a month of storage on the reference profile; amounts are null until prices are set). open takes lines and files; closed takes no more lines; complete once nothing is waiting or queued.
Parameters
idpathrequired Type:stringA record, queued filing or export id.
Returns
200The import400Type: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/imports/{id}" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/imports/{id}",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports/{id}', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Send one line's file
POST /folio/imports/{id}/content
Needs folio:records:write: File records, create folders, extend retention
The request body is the file of the manifest line named by reference. It must match the line's SHA-256 (otherwise 400, and nothing changes). It is then filed through the same pipeline as POST /folio/records (the same scan, rules, ledger and receipt), as bulk work, with the import's provenance and the line's original date. A line already sent is answered as it stands, so a resumed upload changes nothing. While the organisation has as much import work queued as it may, the answer is 429 rate_limited: send it again shortly.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
idpathrequired Type:stringA record, queued filing or export id.
referencequeryrequired Type:stringThe manifest line's reference.
Returns
202Type:FolioImportLineThe line,queued(or aduplicate)400Type: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/imports/{id}/content?reference=<reference>" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.post(
"https://api.unforged.sh/folio/imports/{id}/content?reference=<reference>",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports/{id}/content?reference=<reference>', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); {
"seq": 1,
"reference": "dms/1",
"folder": "archive/2020",
"name": "letter.pdf",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"size": 18234,
"state": "queued",
"record_id": null,
"duplicate_of": null,
"error": null
} An import's lines
GET /folio/imports/{id}/items
Needs folio:records:read: See filed records, folders and metadata
The lines in manifest order, optionally of one state: state=refused and state=failed are the per-line error report, each with why. Follow next_after. Ledgered as import.listed.
Parameters
idpathrequired Type:stringA record, queued filing or export id.
statequeryoptional Type:"awaiting""queued""filed""duplicate""refused""failed"afterqueryoptional Type:integerlimitqueryoptional Type:integer
Returns
200A page of lines400Type: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/imports/{id}/items" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/imports/{id}/items",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports/{id}/items', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Close an import
POST /folio/imports/{id}/close
Needs folio:records:write: File records, create folders, extend retention
The import takes no more lines, and lines still waiting for their file are failed. It completes once its queued files are filed (ledgered as import.closed, then import.completed with the counts).
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
idpathrequired Type:stringA record, queued filing or export id.
Returns
200Type:FolioImportThe import400Type: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 -X POST "https://api.unforged.sh/folio/imports/{id}/close" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.post(
"https://api.unforged.sh/folio/imports/{id}/close",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports/{id}/close', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); An import's report
GET /folio/imports/{id}/report
Needs folio:records:read: See filed records, folders and metadata
The import's counts, every refused or failed line with why, and whether the filed lines reconcile with the ledger: one record.imported entry for each, and no other. Together with the hash manifest (GET /folio/imports/{id}/manifest.csv) it shows what was imported, when, from where and with which hashes. Ledgered as import.reported.
Parameters
idpathrequired Type:stringA record, queued filing or export id.
Returns
200Type:FolioImportReportThe 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/imports/{id}/report" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/imports/{id}/report",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports/{id}/report', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); An import's hash manifest
GET /folio/imports/{id}/manifest.csv
Needs folio:records:read: See filed records, folders and metadata
Every line as CSV: its line number, reference, folder, name, SHA-256, size, state, record, the line it duplicates and any error. Ledgered as import.reported.
Parameters
idpathrequired Type:stringA record, queued filing or export id.
Returns
200The hash manifest400Type: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/imports/{id}/manifest.csv" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/imports/{id}/manifest.csv",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/imports/{id}/manifest.csv', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); List folio S3 keys
GET /folio/s3/keys
Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks
The access keys S3 clients use with folio S3 that can still be used: long-lived keys not revoked, and reason-session credentials not yet expired. Never a secret. Also names the organisation's bucket and region.
Returns
200Type:FolioS3KeyListThe keys400Type: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/s3/keys" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/folio/s3/keys",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/s3/keys', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Create a folio S3 key
POST /folio/s3/keys
Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks
A long-lived access key for S3 clients (SigV4), acting as s3_key:<id>. Its permissions are folio permissions within yours. The secret is shown once. No Idempotency-Key: a retry creates another key. At most 20 keys at once (409).
Request body
nameoptional Type:stringnullpermissionsrequired Type:string[]
Returns
201Type:FolioS3KeyCreatedCreated400Type: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/s3/keys" \
-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/s3/keys",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/s3/keys', {
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()); Revoke a folio S3 key
DELETE /folio/s3/keys/{id}
Needs folio:settings:manage: Retention classes, reason settings, protected folders, webhooks
The key stops working at once. 404 for an unknown or already revoked key.
Parameters
idpathrequired Type:string
Returns
204Revoked400Type: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 -X DELETE "https://api.unforged.sh/folio/s3/keys/{id}" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.delete(
"https://api.unforged.sh/folio/s3/keys/{id}",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/s3/keys/{id}', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Open a reason session for S3 tools
POST /folio/s3/sessions
Needs folio:records:read: See filed records, folders and metadata
For S3 tools that cannot send reason headers: opens a reason as POST /folio/reasons does (at most 20 minutes) and returns short-lived S3 credentials bound to it. They act as you, reach only the reason's context (a folder and its sub-folders, a matter, a subject or records), and stop when the reason ends. permissions default to your folio permissions. The secret is shown once; no Idempotency-Key.
Request body
coderequired Type:stringOne of the organisation's reason codes.
textoptional Type:stringnullmatteroptional Type:stringnullcontextrequired Type:objectsecondsoptional Type:integerRequested lifetime; capped by the organisation and at 20 minutes.
permissionsoptional Type:string[]null
Returns
201Type:FolioS3SessionOpened400Type: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_found413Type:Problemtoo_large421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/folio/s3/sessions" \
-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/s3/sessions",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/folio/s3/sessions', {
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());