curl --request POST \
--url https://app.sahlfinancial.com/api/v1/kyc/extract \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form files=@example-file \
--form doc_type=payslip \
--form step_key=proof_of_address \
--form reference=client-0001 \
--form environment=sandbox \
--form 'subject=Test Client' \
--form kind=individualconst form = new FormData();
form.append('files', '<string>');
form.append('doc_type', 'payslip');
form.append('step_key', 'proof_of_address');
form.append('reference', 'client-0001');
form.append('environment', 'sandbox');
form.append('subject', 'Test Client');
form.append('kind', 'individual');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://app.sahlfinancial.com/api/v1/kyc/extract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.sahlfinancial.com/api/v1/kyc/extract"
files = { "files": ("example-file", open("example-file", "rb")) }
payload = {
"doc_type": "payslip",
"step_key": "proof_of_address",
"reference": "client-0001",
"environment": "sandbox",
"subject": "Test Client",
"kind": "individual"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text){
"fields": {
"first_name": "Test",
"last_name": "Client",
"document_holder_name": "Test Client",
"employer_name": "Test Employer SARL",
"occupation": "Analyst",
"document_date": "2026-09-30"
},
"documents": [
{
"filename": "payslip-test.pdf",
"doc_type": "payslip",
"step_hint": "payslip",
"step_key": null,
"fields": {
"first_name": "Test",
"last_name": "Client",
"document_holder_name": "Test Client",
"employer_name": "Test Employer SARL",
"occupation": "Analyst",
"document_date": "2026-09-30"
},
"meta_created": "2026-10-01",
"meta_provenance": {
"producer": "Example Payroll 4.2",
"revisions": 1
},
"mapped": 6,
"notes": [],
"document_id": "22222222-2222-4222-8222-222222222222"
}
],
"field_count": 6,
"checks": [],
"reader_unavailable": false,
"policy": {
"id": null,
"version": 0,
"source": "legacy",
"regime": "none",
"regulator": null,
"purpose": "onboarding",
"overrides_refused": []
},
"case_id": "00000000-0000-4000-8000-000000000001",
"document_ids": [
"22222222-2222-4222-8222-222222222222"
]
}{
"detail": "Send between 1 and 5 files."
}{
"detail": "Invalid or revoked API key"
}{
"detail": {
"code": "kyc_scope_not_allowed",
"message": "This workspace is not enabled for the partner KYC API."
}
}{
"detail": "File too large. Maximum allowed size is 30 MB."
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"detail": {
"code": "kyc_extract_cap_reached",
"message": "Monthly document-read limit of 2000 reached.",
"used": 2000,
"limit": 2000
}
}{
"code": "internal_error",
"message": "An unexpected error occurred",
"details": null
}Read documents
Required scope: kyc:extract.
Read one upload step’s document(s) into canonical fields, with checks.
doc_type hints the reader which document this is. step_key names
the step in a language-independent way; it decides which document types
the step accepts.
With a reference, the files and what was read from them are also filed
on that client’s case in environment (‘sandbox’ or ‘production’), and
the answer carries case_id and each document’s document_id.
kind (the client kind, as /verify takes it) picks the KYC or KYB
policy whose thresholds the document checks use; omitted, the KYC one.
Guides: Read documents, Document types and fields.
curl --request POST \
--url https://app.sahlfinancial.com/api/v1/kyc/extract \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form files=@example-file \
--form doc_type=payslip \
--form step_key=proof_of_address \
--form reference=client-0001 \
--form environment=sandbox \
--form 'subject=Test Client' \
--form kind=individualconst form = new FormData();
form.append('files', '<string>');
form.append('doc_type', 'payslip');
form.append('step_key', 'proof_of_address');
form.append('reference', 'client-0001');
form.append('environment', 'sandbox');
form.append('subject', 'Test Client');
form.append('kind', 'individual');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://app.sahlfinancial.com/api/v1/kyc/extract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.sahlfinancial.com/api/v1/kyc/extract"
files = { "files": ("example-file", open("example-file", "rb")) }
payload = {
"doc_type": "payslip",
"step_key": "proof_of_address",
"reference": "client-0001",
"environment": "sandbox",
"subject": "Test Client",
"kind": "individual"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text){
"fields": {
"first_name": "Test",
"last_name": "Client",
"document_holder_name": "Test Client",
"employer_name": "Test Employer SARL",
"occupation": "Analyst",
"document_date": "2026-09-30"
},
"documents": [
{
"filename": "payslip-test.pdf",
"doc_type": "payslip",
"step_hint": "payslip",
"step_key": null,
"fields": {
"first_name": "Test",
"last_name": "Client",
"document_holder_name": "Test Client",
"employer_name": "Test Employer SARL",
"occupation": "Analyst",
"document_date": "2026-09-30"
},
"meta_created": "2026-10-01",
"meta_provenance": {
"producer": "Example Payroll 4.2",
"revisions": 1
},
"mapped": 6,
"notes": [],
"document_id": "22222222-2222-4222-8222-222222222222"
}
],
"field_count": 6,
"checks": [],
"reader_unavailable": false,
"policy": {
"id": null,
"version": 0,
"source": "legacy",
"regime": "none",
"regulator": null,
"purpose": "onboarding",
"overrides_refused": []
},
"case_id": "00000000-0000-4000-8000-000000000001",
"document_ids": [
"22222222-2222-4222-8222-222222222222"
]
}{
"detail": "Send between 1 and 5 files."
}{
"detail": "Invalid or revoked API key"
}{
"detail": {
"code": "kyc_scope_not_allowed",
"message": "This workspace is not enabled for the partner KYC API."
}
}{
"detail": "File too large. Maximum allowed size is 30 MB."
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"detail": {
"code": "kyc_extract_cap_reached",
"message": "Monthly document-read limit of 2000 reached.",
"used": 2000,
"limit": 2000
}
}{
"code": "internal_error",
"message": "An unexpected error occurred",
"details": null
}Authorizations
API key created in the console. Scopes: kyc:extract, kyc:verify, kyc:eid; bank:read, bank:write (Growth plan, enabled per workspace by Sahl).
Body
1 to 5 files per call. JPEG, PNG, WebP, TIFF or PDF, up to 30 MB each.
1 to 5 files. JPEG, PNG, WebP, TIFF or PDF, up to 30 MB each. The content must match the declared type.
Hint for the reader about which document this is, for example payslip or national_id. Applies to every file in the call, so send one document type per call.
"payslip"
Stable name of your upload step, for example photo_id or proof_of_address. It decides which document types the step accepts. Optional.
"proof_of_address"
Your own id for the client: 1 to 64 characters of A-Z a-z 0-9 _ . : -. With one, the files, the fields and the verdict are filed on a case in your workspace. Without one, nothing is filed.
"client-0001"
sandbox (default) or production. Cases are separate per environment.
sandbox, production "sandbox"
Client name for the case, up to 255 characters.
"Test Client"
Client kind: individual, corporation, partnership, charitable_org, trust, estate. Picks the individual or entity policy thresholds. Omitted, the individual policy applies.
"individual"
Response
OK
Fields merged across the files of the call. The first non-empty value wins, in file order, so send the strongest identity document first.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Number of keys in fields.
The per-document checks of every file, in one list.
Show child attributes
Show child attributes
True when at least one file was never read (a problem on the Sahl side). It does not mean the document was blank.
Which workspace policy the call ran under, and which request switches it refused.
Show child attributes
Show child attributes
Only with a reference.
Only with a reference. Same order as documents.