Checks
Check documents, see what was found and record the decision.
The LoupeCheck object
A check. result is set once status is completed, and error says why when it is failed. documents_deleted_at is when we deleted the documents; what the check found stays. folio says where they went once someone asked for them to be kept in Folio.
Attributes
idrequired Type:stringstatusrequired Type:"queued""running""completed""failed"created_byrequired Type:stringcreated_atrequired Type:stringcompleted_atrequired Type:stringnulldocuments_deleted_atrequired Type:stringnulldocumentsrequired Type:LoupeDocument[]policyrequired Type:LoupePolicyresultrequired Type:LoupeResultnullerrorrequired Type:stringnullreviewrequired Type:LoupeReviewnullassigned_torequired Type:stringnullWho is deciding on the check.
assigned_atrequired Type:stringnullfoliorequired Type:LoupeFolioKeptnull
{
"id": "lck_01jb2r6x8k4m7q9s2t5v8w1y3z",
"status": "queued",
"created_by": "api_key:key_01jb2r5n3c8d6f9h2j4k7m1p5r",
"created_at": "2026-10-22T09:14:03Z",
"completed_at": null,
"documents_deleted_at": null,
"documents": [
{
"index": 0,
"name": "statement-september.pdf",
"sha256": "3f1c9a0e5b7d2c4f6a8b1d3e5f7a9c0b2d4e6f8a1c3e5b7d9f0a2c4e6b8d1f3a",
"size_bytes": 184211
}
],
"policy": {
"name": "default",
"version": 1,
"fail": 0.9,
"flag": 0.5,
"auto": 0.15,
"auto_approve": true,
"weights": {},
"disabled": []
},
"result": null,
"error": null,
"review": null,
"assigned_to": null,
"assigned_at": null,
"folio": null
} The LoupeResult object
What the check found. signals here are the findings that involve more than one document.
Attributes
verdictrequired Type:LoupeVerdictprobabilityrequired Type:numbertop_reasonsrequired Type:string[]ratingoptional Type:LoupeRatingtrusted_becauserequired Type:LoupeAssurance[]What was checked across the documents sent together and held.
documentsrequired Type:LoupeDocumentResult[]signalsrequired Type:LoupeSignal[]versionsrequired Type:LoupeVersions
List checks
GET /loupe/checks
Needs loupe:checks:read: See document checks and their findings
Newest first, 50 a page (limit up to 200); next_cursor continues the list. With queue=waiting you get the checks that still need someone to decide, oldest first, and with queue=decided the ones your team has decided, latest decision first; assigned_to narrows either to me, nobody or one member.
Parameters
cursorqueryoptional Type:stringThe
next_cursorof the page before.limitqueryoptional Type:integerqueuequeryoptional Type:"waiting""decided"assigned_toqueryoptional Type:stringme,nobodyor a member asuser:…. Only withqueue.qqueryoptional Type:stringA document name to look for, forgiving small typos. You get the newest checks with a document named like it, each with those documents, on one page.
Returns
200Type:LoupeCheckPageA page of checks400Type: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/loupe/checks" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/loupe/checks",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Check documents
POST /loupe/checks
Needs loupe:checks:create: Send documents to be checked for forgery
Send each document as a file part: a PDF, an image (PNG, JPEG, TIFF, WebP or HEIC), a Word or Excel file, an email saved as .eml, or one ZIP holding several (up to 500 documents, 50 MB in one request). An email's attachments are checked as documents of their own. Documents sent together are checked together, so a page that turns up twice or details that don't agree between them are found too. The check runs straight away and usually finishes within a minute; get it by its id, or list your checks, to see what it found. We delete the documents 24 hours after the check and keep what it found. A policy part (JSON) picks one of your saved policies by name (and version, for one other than the latest), or gives a policy in full. Without one, the check uses your saved default policy, or ours if you haven't saved one.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
Returns
202Type:LoupeCheckThe check, queued400Type:Probleminvalid_request,invalid_version,idempotency_key_requiredorinvalid_signature401Type:Problemunauthorized402Type:Problembilling_required403Type: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_large415Type:Problemunsupported_media421Type:Problemwrong_region429Type:Problemrate_limitedorquota_exceededdefaultType:Probleminternal
curl -X POST "https://api.unforged.sh/loupe/checks" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.post(
"https://api.unforged.sh/loupe/checks",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); {
"id": "lck_01jb2r6x8k4m7q9s2t5v8w1y3z",
"status": "queued",
"created_by": "api_key:key_01jb2r5n3c8d6f9h2j4k7m1p5r",
"created_at": "2026-10-22T09:14:03Z",
"completed_at": null,
"documents_deleted_at": null,
"documents": [
{
"index": 0,
"name": "statement-september.pdf",
"sha256": "3f1c9a0e5b7d2c4f6a8b1d3e5f7a9c0b2d4e6f8a1c3e5b7d9f0a2c4e6b8d1f3a",
"size_bytes": 184211
}
],
"policy": {
"name": "default",
"version": 1,
"fail": 0.9,
"flag": 0.5,
"auto": 0.15,
"auto_approve": true,
"weights": {},
"disabled": []
},
"result": null,
"error": null,
"review": null,
"assigned_to": null,
"assigned_at": null,
"folio": null
} Get a check
GET /loupe/checks/{check_id}
Needs loupe:checks:read: See document checks and their findings
The check and, once it has finished, what it found: a verdict for the whole submission and for each document, the reasons, and each finding with where it is on the page.
Parameters
check_idpathrequired Type:stringThe check's id.
Returns
200Type:LoupeCheckThe check401Type: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/loupe/checks/{check_id}" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/loupe/checks/{check_id}",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks/{check_id}', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); {
"id": "lck_01jb2r6x8k4m7q9s2t5v8w1y3z",
"status": "queued",
"created_by": "api_key:key_01jb2r5n3c8d6f9h2j4k7m1p5r",
"created_at": "2026-10-22T09:14:03Z",
"completed_at": null,
"documents_deleted_at": null,
"documents": [
{
"index": 0,
"name": "statement-september.pdf",
"sha256": "3f1c9a0e5b7d2c4f6a8b1d3e5f7a9c0b2d4e6f8a1c3e5b7d9f0a2c4e6b8d1f3a",
"size_bytes": 184211
}
],
"policy": {
"name": "default",
"version": 1,
"fail": 0.9,
"flag": 0.5,
"auto": 0.15,
"auto_approve": true,
"weights": {},
"disabled": []
},
"result": null,
"error": null,
"review": null,
"assigned_to": null,
"assigned_at": null,
"folio": null
} Download the evidence pack
GET /loupe/checks/{check_id}/evidence
Needs loupe:checks:read: See document checks and their findings
The check as a PDF to keep with your case: the result, each finding marked on its page, what each file says about itself, the policy and versions that gave the verdict, and the decision your team recorded. It stays available after we've deleted the documents, and from then on lists each finding and where it was without the pages.
Parameters
check_idpathrequired Type:stringThe check's id.
Returns
200The evidence pack400Type: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/loupe/checks/{check_id}/evidence" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/loupe/checks/{check_id}/evidence",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks/{check_id}/evidence', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Record the decision
POST /loupe/checks/{check_id}/review
Needs loupe:checks:review: Record the decision on a document check
What the person who looked at the documents decided, and a note saying why. We never reject a document on the check alone, so this is where the decision is kept; sending a new one replaces it. A check can be reviewed once it has finished.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
check_idpathrequired Type:stringThe check's id.
Request body
outcomerequired Type:"genuine""fraud""inconclusive"noterequired Type:stringWhy you reached this decision.
Returns
200Type:LoupeCheckThe check, with the decision400Type: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/loupe/checks/{check_id}/review" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{
"outcome": "fraud",
"note": "The closing balance doesn'\''t match the copy the bank sent us."
}' import requests
r = requests.post(
"https://api.unforged.sh/loupe/checks/{check_id}/review",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={
"outcome": "fraud",
"note": "The closing balance doesn't match the copy the bank sent us."
},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks/{check_id}/review', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({
"outcome": "fraud",
"note": "The closing balance doesn't match the copy the bank sent us."
}),
});
console.log(await res.json()); {
"id": "lck_01jb2r6x8k4m7q9s2t5v8w1y3z",
"status": "queued",
"created_by": "api_key:key_01jb2r5n3c8d6f9h2j4k7m1p5r",
"created_at": "2026-10-22T09:14:03Z",
"completed_at": null,
"documents_deleted_at": null,
"documents": [
{
"index": 0,
"name": "statement-september.pdf",
"sha256": "3f1c9a0e5b7d2c4f6a8b1d3e5f7a9c0b2d4e6f8a1c3e5b7d9f0a2c4e6b8d1f3a",
"size_bytes": 184211
}
],
"policy": {
"name": "default",
"version": 1,
"fail": 0.9,
"flag": 0.5,
"auto": 0.15,
"auto_approve": true,
"weights": {},
"disabled": []
},
"result": null,
"error": null,
"review": null,
"assigned_to": null,
"assigned_at": null,
"folio": null
} Assign a check
POST /loupe/checks/{check_id}/assign
Needs loupe:checks:review: Record the decision on a document check
Gives the check to the person who'll decide on it: me, a member as user:…, or null to take it off whoever has it.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
check_idpathrequired Type:stringThe check's id.
Request body
torequired Type:stringnullme, a member asuser:…, ornullfor nobody.
Returns
200Type:LoupeCheckThe check, assigned400Type: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/loupe/checks/{check_id}/assign" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{
"to": "me"
}' import requests
r = requests.post(
"https://api.unforged.sh/loupe/checks/{check_id}/assign",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={
"to": "me"
},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks/{check_id}/assign', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({
"to": "me"
}),
});
console.log(await res.json()); {
"id": "lck_01jb2r6x8k4m7q9s2t5v8w1y3z",
"status": "queued",
"created_by": "api_key:key_01jb2r5n3c8d6f9h2j4k7m1p5r",
"created_at": "2026-10-22T09:14:03Z",
"completed_at": null,
"documents_deleted_at": null,
"documents": [
{
"index": 0,
"name": "statement-september.pdf",
"sha256": "3f1c9a0e5b7d2c4f6a8b1d3e5f7a9c0b2d4e6f8a1c3e5b7d9f0a2c4e6b8d1f3a",
"size_bytes": 184211
}
],
"policy": {
"name": "default",
"version": 1,
"fail": 0.9,
"flag": 0.5,
"auto": 0.15,
"auto_approve": true,
"weights": {},
"disabled": []
},
"result": null,
"error": null,
"review": null,
"assigned_to": null,
"assigned_at": null,
"folio": null
} Keep a check's documents in Folio
POST /loupe/checks/{check_id}/folio
Needs loupe:checks:read: See document checks and their findings
Files the check's documents and its evidence pack in a Folio folder, under the retention class you choose or the folder's own. You also need folio:records:write, and Folio switched on for your organisation. The check answers straight away with folio.state set to queued; it reads filed once they're in Folio, or failed if they couldn't go, when you can ask again. You can't keep a check's documents while it's running, after they've been deleted, or a second time.
Parameters
Idempotency-Keyheaderrequired Type:string1–255 printable ASCII characters, kept 24 hours.
check_idpathrequired Type:stringThe check's id.
Request body
folderrequired Type:stringThe folder in Folio, such as
Fraud cases/2026; a new one is made if it isn't there.classoptional Type:stringnullThe retention class's id; the folder's own when left out.
nameoptional Type:stringnullThe name in Folio of a check's only document; with several
Returns
202Type:LoupeCheckThe check, its documents on their way to Folio400Type: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/loupe/checks/{check_id}/folio" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" \
-H "Content-Type: application/json" \
-d '{
"folder": "Fraud cases/2026",
"class": null,
"name": null
}' import requests
r = requests.post(
"https://api.unforged.sh/loupe/checks/{check_id}/folio",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
json={
"folder": "Fraud cases/2026",
"class": None,
"name": None
},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks/{check_id}/folio', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
body: JSON.stringify({
"folder": "Fraud cases/2026",
"class": null,
"name": null
}),
});
console.log(await res.json()); {
"id": "lck_01jb2r6x8k4m7q9s2t5v8w1y3z",
"status": "queued",
"created_by": "api_key:key_01jb2r5n3c8d6f9h2j4k7m1p5r",
"created_at": "2026-10-22T09:14:03Z",
"completed_at": null,
"documents_deleted_at": null,
"documents": [
{
"index": 0,
"name": "statement-september.pdf",
"sha256": "3f1c9a0e5b7d2c4f6a8b1d3e5f7a9c0b2d4e6f8a1c3e5b7d9f0a2c4e6b8d1f3a",
"size_bytes": 184211
}
],
"policy": {
"name": "default",
"version": 1,
"fail": 0.9,
"flag": 0.5,
"auto": 0.15,
"auto_approve": true,
"weights": {},
"disabled": []
},
"result": null,
"error": null,
"review": null,
"assigned_to": null,
"assigned_at": null,
"folio": null
} Get a document
GET /loupe/checks/{check_id}/documents/{index}
Needs loupe:checks:read: See document checks and their findings
The document as it was sent, until we delete it 24 hours after the check.
Parameters
check_idpathrequired Type:stringThe check's id.
indexpathrequired Type:integerThe document's
indexin the check.
Returns
200The document400Type: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/loupe/checks/{check_id}/documents/{index}" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/loupe/checks/{check_id}/documents/{index}",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks/{check_id}/documents/{index}', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json()); Get a page as an image
GET /loupe/checks/{check_id}/documents/{index}/pages/{page}
Needs loupe:checks:read: See document checks and their findings
One page of a scanned or photographed document as a PNG, at the size we read it, so a finding's box in pixels falls in the right place. Pages are counted from 1. A PDF page that has no picture on it has no image; use its PDF instead.
Parameters
check_idpathrequired Type:stringThe check's id.
indexpathrequired Type:integerThe document's
indexin the check.pagepathrequired Type:integer
Returns
200The page400Type: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/loupe/checks/{check_id}/documents/{index}/pages/{page}" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-10-14" import requests
r = requests.get(
"https://api.unforged.sh/loupe/checks/{check_id}/documents/{index}/pages/{page}",
headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json()) const res = await fetch('https://api.unforged.sh/loupe/checks/{check_id}/documents/{index}/pages/{page}', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());