Developer documentation
API Reference
IdScanly Identity Document Scanning API - v1
The IdScanly API accepts one or more identity document images in a session, processes them asynchronously, and exposes partial and final recognition results through polling or an optional wait-on-upload mode.
Recommended integration
Recommended session-based integration workflow
Multiple images contribute to one merged result. For example, a first photo may show the left side clearly while a retake makes the right side readable. IdScanly combines and ranks field candidates across the session, so the result can use evidence from both photos.
- Create a session
Keep all sides and retakes of the same identity document in this session. Reuse its
sessionIdfor every upload. - Upload the first image immediately
As soon as the first photo is ready, upload it with
wait=false. Processing can begin while the user captures the next side. - Show the photo and help the user check it
Display a preview. Implement basic checks for sharpness, lighting and full-document framing in your own application to help the user decide whether to retake the photo.
- Add retakes and the remaining sides
Upload each new photo to the same session with
wait=false. Keep the earlier uploads: readable parts of a weaker photo can complement a clearer retake. - Display progressively available fields
Read partial results with
GET /api/v1/sessions/{sessionId}?view=summary. Earlier uploads may provide only some fields; additional sides can add data. Await=falseupload returns a202acknowledgement, not recognition results. - Wait on the known final image
Upload the last photo with
wait=true&view=summary. While that request is waiting, one parallel GET can show the currently available partial data. Do not let a late partial response overwrite the completed response.
A session accepts up to 10 images by default, including all sides and retakes. Every accepted image can contribute; a weaker photo does not have to be discarded just because another photo is clearer. Keep API keys on your backend.
Wait mode does not seal the session. Finish the earlier uploads before sending the final image, and do not append images during the final wait. After five minutes, a 202 response with waitTimedOut: true requires continued polling; waiting does not guarantee a successful read. See wait mode and timeout handling and the server-side JavaScript example.
Base URL
https://bck.idscanly.com/api/v1All examples below use:
BASE_URL="https://bck.idscanly.com/api/v1"
API_KEY="idscanly_live_replace_me"Authentication
Send the API key in the x-api-key header on every request.
x-api-key: idscanly_live_replace_mecurl -H "x-api-key: $API_KEY" "$BASE_URL/stats/today"Missing, invalid, disabled, or expired keys return:
HTTP/1.1 401 Unauthorized
Cache-Control: no-store{
"error": "Invalid or expired API key",
"code": "UNAUTHORIZED",
"requestId": "req-123"
}API keys are provisioned by IdScanly. Contact support and provide the partner or application name, technical contact, and intended environment.
Image Requirements
| Property | Description |
|---|---|
Multipart Content-Type | multipart/form-data |
Multipart field name | image |
Base64 Content-Type | application/json |
Max file size | 15 MiB |
Max decoded base64 image size | 15 MiB |
Max images per session | 10 (default) |
Images per request | One image per upload request |
Supported formats | JPEG, PNG, WebP, HEIC, HEIF |
The server validates actual image content, not only the MIME header. HEIC/HEIF uploads are decoded server-side before preprocessing and OCR.
Recommended capture
- •Show the entire document - avoid cut-off corners
- •Use sufficient light without glare
- •Keep text reasonably sharp
- •Use the highest practical resolution
- •Upload front and back as separate images in the same session
1. Create a Session
/v1/sessionsNo request body is required. Returns a session ID and upload URL.
curl -X POST \
-H "x-api-key: $API_KEY" \
"$BASE_URL/sessions"Response 201 Created
{
"success": true,
"sessionId": "9c5c6e9c-73b6-4976-a84f-527a98552713",
"isProcessing": false,
"status": "waiting_for_images",
"uploadUrl": "/v1/sessions/9c5c6e9c-73b6-4976-a84f-527a98552713/images",
"resultUrl": "/v1/sessions/9c5c6e9c-73b6-4976-a84f-527a98552713"
}2. Upload an Image
/v1/sessions/{sessionId}/imagesSESSION_ID="9c5c6e9c-73b6-4976-a84f-527a98552713"
curl -X POST \
-H "x-api-key: $API_KEY" \
-F "image=@front.jpg" \
"$BASE_URL/sessions/$SESSION_ID/images"Optional document crop
Enable automatic ID detection and cropping with the crop=true query parameter. Accepted values: true, false, 1, 0, yes, no, on, and off. Disabled by default.
curl -X POST \
-H "x-api-key: $API_KEY" \
-F "image=@front.jpg" \
"$BASE_URL/sessions/$SESSION_ID/images?crop=true"Use crop when the ID occupies only part of a larger photograph. Leave disabled when the image is already tightly framed.
Response 202 Accepted
{
"success": true,
"imageId": "5f4fc156-c08d-4cd0-b9f6-5112eb13178e",
"sessionId": "9c5c6e9c-73b6-4976-a84f-527a98552713",
"status": "queued",
"cropEnabled": false,
"resultUrl": "/v1/sessions/9c5c6e9c-73b6-4976-a84f-527a98552713"
}202 means accepted for processing. It does not mean recognition has finished.
Optionally wait for the final result
Add wait=true only when uploading the known final image of a session. The request remains open until every queued or running image in that session finishes. Use view=summary for the compact response.
curl -X POST \
-H "x-api-key: $API_KEY" \
-F "image=@back.jpg" \
"$BASE_URL/sessions/$SESSION_ID/images?wait=true&view=summary"Accepted values are true, false, 1 and 0. Without this option, uploads retain the immediate 202 behavior.
Completed response 200 OK
{
"success": true,
"sessionId": "9c5c6e9c-73b6-4976-a84f-527a98552713",
"status": "finished",
"isProcessing": false,
"result": { "fields": { "documentNumber": "123456AB" } },
"imageId": "5f4fc156-c08d-4cd0-b9f6-5112eb13178e",
"cropEnabled": false,
"resultUrl": "/v1/sessions/9c5c6e9c-73b6-4976-a84f-527a98552713",
"waitTimedOut": false
}202 Accepted with waitTimedOut: true, Retry-After: 2 and resultUrl. Processing continues, so poll the result URL. Configure client and proxy timeouts to at least 310 seconds.Upload earlier images asynchronously and do not append more images after starting the final wait request. Wait mode does not seal the session.
Upload a base64 image
Use the dedicated base64 endpoint when the client cannot send multipart form data.
/v1/sessions/{sessionId}/images/base64IMAGE_BASE64="$(base64 -w 0 front.jpg)"
curl -X POST \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d "{\"imageBase64\":\"$IMAGE_BASE64\",\"filename\":\"front.jpg\",\"mimeType\":\"image/jpeg\"}" \
"$BASE_URL/sessions/$SESSION_ID/images/base64"JSON body
{
"imageBase64": "/9j/4AAQSkZJRgABAQ...",
"filename": "front.jpg",
"mimeType": "image/jpeg"
}imageBase64 may also be a data URL. When a data URL is used, mimeType is optional. The response is the same 202 Accepted payload returned by multipart uploads. Base64 uploads also support wait=true&view=summary.
Add more images
Upload each additional image (e.g. back of the document) in a separate request using the same session ID. Do not create a separate session for each side of the same document.
curl -X POST \
-H "x-api-key: $API_KEY" \
-F "image=@back.jpg" \
"$BASE_URL/sessions/$SESSION_ID/images"Upload without explicit session creation
A compatibility endpoint that creates a session automatically:
# Creates a new session automatically
curl -X POST \
-H "x-api-key: $API_KEY" \
-F "image=@front.jpg" \
"$BASE_URL/session-images"
# Append to an existing session
curl -X POST \
-H "x-api-key: $API_KEY" \
-F "image=@back.jpg" \
"$BASE_URL/session-images?sessionId=$SESSION_ID"For new integrations, the explicit POST /sessions → POST /sessions/{sessionId}/images workflow is preferred.
The same compatibility behavior is available for base64 uploads. Both compatibility endpoints accept wait=true; use it only on the final image:
curl -X POST \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d "{\"imageBase64\":\"$IMAGE_BASE64\",\"filename\":\"front.jpg\",\"mimeType\":\"image/jpeg\"}" \
"$BASE_URL/session-images/base64"3. Poll Results
/v1/sessions/{sessionId}?view=summarycurl \
-H "x-api-key: $API_KEY" \
"$BASE_URL/sessions/$SESSION_ID?view=summary"isProcessing is false. Partial fields appear while processing is still active.Example response (still processing)
{
"success": true,
"sessionId": "22222222-2222-4222-8222-222222222222",
"status": "processing",
"isProcessing": true,
"summary": null,
"result": null,
"images": [
{
"imageId": "11111111-1111-4111-8111-111111111111",
"status": "running",
"sourceName": "example-front.jpg",
"fieldCount": 0,
"avgConfidence": 0,
"mrzFound": false,
"hasExternalResult": false,
"externalResultUpdatedAt": null,
"error": null,
"updatedAt": "2026-09-05T10:00:00.000Z",
"completedAt": null
}
],
"createdAt": "2026-09-05T10:00:00.000Z",
"updatedAt": "2026-09-05T10:00:00.000Z",
"completedAt": null
}Full diagnostic view
Omitting ?view=summary returns the full response including per-image OCR blocks, candidates, recognition layer details, preprocessing timings and logs. Use only for controlled diagnostics.
/v1/sessions/{sessionId}Session Status Values
| Status | Meaning |
|---|---|
waiting_for_images | Session exists but no image has been uploaded |
processing | At least one image is queued or running |
finished | No image is processing; at least one image completed |
error | All images in the session failed |
isProcessing === false. A finished session may contain individual failed images - check result.failedImageCount and each image status.Image status values
| Status | Meaning |
|---|---|
queued | Accepted and waiting to start |
running | Image processing is active |
done | Processing completed successfully |
error | Processing failed for this image |
Response JSON reference
Read this reference alongside the endpoint examples. It distinguishes the response envelope, the merged document result, and image-level diagnostic data; these objects have different meanings even when their property names overlap.
Top-level response envelope
This schema describes GET /api/v1/sessions/{sessionId}, including ?view=summary, and the completed or timed-out wait=true upload response. Session creation and immediate 202 upload acknowledgements have their own smaller payloads shown above.
summary and result contain the same merged object in both views for backwards compatibility. Read one of them, for example payload.result ?? payload.summary. They are null until a merged result is available; an existing snapshot can also have empty fields. Partial values can change while processing continues.
| Property | Description |
|---|---|
success | boolean. Exactly status !== "error" for this envelope. It does not mean that OCR succeeded or all required fields were found; a waiting or processing session can have success: true. HTTP errors are handled separately. |
sessionId | string. Session UUID; use it to upload additional images and poll the same session. |
status | string. waiting_for_images, processing, finished, or error. This is the processing lifecycle, not the merged read-quality state. |
isProcessing | boolean. Whether queued or running images remain. Stop polling the current batch when false. A new session also starts with false; it does not mean an image has been read. Uploading another image may start processing again. |
summary / result | object | null. Two aliases for the same merged result described below; view=summary does not remove result or shorten the merged object. |
images | array. One transport-level entry per uploaded image. Its shape depends on view; it is different from result.images. |
logs | array. Session diagnostic logs in the full view only. Omitted in view=summary. Log content is diagnostic and may evolve. |
createdAt / updatedAt / completedAt | Timestamp strings serialized by the API, or null when unavailable. completedAt is the lifecycle completion timestamp; do not use it instead of isProcessing to decide whether the current batch is done. |
imageId / cropEnabled / resultUrl / waitTimedOut | Added by a wait=true upload response, not normal GET polling. imageId identifies that upload; cropEnabled is boolean; resultUrl is the polling path. waitTimedOut: true means HTTP 202 and Retry-After: 2; processing continues. false means HTTP 200 after waiting finished, which still does not guarantee a successful read. |
Summary view and full view
Use ?view=summary for normal integration. Omitting view, or using any value other than the exact string summary, returns the full diagnostic view.
There are three distinct image paths: payload.images describes upload/processing records; payload.result.images describes per-image read quality; payload.images[i].images contains image URLs only in full view. Join the first two arrays by imageId, not by their position.
| Property | Description |
|---|---|
Shared fields in payload.images[] | imageId: string; status: queued | running | done | error; sourceName: string; hasExternalResult: boolean; externalResultUpdatedAt: timestamp | null; error: string | null; updatedAt and completedAt: timestamps or null. |
Summary-only image fields | fieldCount: number of keys in OCR fields (0 when absent); avgConfidence: number (0 when absent); mrzFound: boolean. Full view exposes these through ocr instead. |
Full-only image fields | images: { original, processed, cropped } with URL strings in the response or null; detection: object | null; ocr: object | null; externalResult: object | array | null; externalResultCreatedAt: timestamp | null. |
externalResult | Caller-supplied comparison data when submitted through the external-result endpoint. It is separate from the scanner result.fields. hasExternalResult can be checked without requesting the full object. |
Missing images and OCR | A queued, failed, or partially processed record can lack OCR, detection or an image variant. Treat null and absent optional properties defensively. Empty arrays mean no entries in the response, not necessarily a processing error. |
Merged result object: result and summary
The merger selects field candidates across the session. Fields from different document sides can complement each other. The result is a snapshot, not a promise that every field printed on a document was extracted.
Counts and read-quality classifications describe image results. While processing, an image with partial OCR may already have a read-quality decision; successfulImageCount + failedImageCount can be smaller than imageCount when no decision is available.
| Property | Description |
|---|---|
status | empty: no images; partial_or_complete: at least one image currently passes the read-quality checks, even if other images are still processing; failed: all images are terminal and at least one failed the read-quality checks, with none successful; processing: the remaining cases. |
isComplete | boolean. True only when there is at least one image and every image status is done or error. It describes processing completion, not field completeness or read success. |
imageCount / successfulImageCount / failedImageCount | Integers. Total uploaded images, images whose readSuccess is true, and images whose readSuccess is false, respectively. A done image can count as a failed read. |
fieldCount | Integer. Number of selected fields before adding displayName and nameOrder. Metadata and fullName can count; derived display fields do not. It can therefore be smaller than Object.keys(fields).length. |
runtime | Object with startedAt, finishedAt, durationMs. startedAt is the earliest image creation time; finishedAt is the latest available image completion time; durationMs is their non-negative difference in milliseconds, or null if either endpoint is unavailable. Includes elapsed queue time, not just OCR execution. finishedAt can already be set while other images are processing. |
fields | Object mapping optional field names to selected values. Normally strings; visibleDates is an array. Preserve identifier strings and leading zeros. Missing extraction generally means the key is absent. Names, dates, units and document-specific values follow the field notes below. |
fieldConfidences | Object mapping the same field names to numeric confidence scores (normally 0?1). 0.97 is a confidence score, not a guarantee or a calibrated 97% probability. Missing source confidence falls back to 0; session scores are rounded to four decimal places. |
fieldSources | Object keyed by field name, describing the selected value and its origin. See the source-entry schema below. It is not a list of every competing candidate. |
documents | Array of document classification objects. The merger may place a session-selected classification first and include differing image classifications; this is not a one-to-one image list. |
mrz | Array of MRZ objects with found: true from non-error OCR images. It can be empty, contain repeated readings, or contain failed check digits. found alone does not establish validity; inspect validCheckDigits and confidence. |
images | Array of per-image read summaries; see the result.images[] table. Present in both views. |
captureFeedback | Object with status and items. Aggregates capture guidance across images; items include imageIds. See Capture Feedback for every code, severity, item status, evidence and suggestedAction. |
flags | Array of unique operational, extractor-coverage or field-quality codes. A flag alone does not set requiresReview. See the decision guidance below. |
requiresReview / reviewReasons | boolean and string array. requiresReview is true exactly when reviewReasons is non-empty. Reasons summarize read failures and identity conflicts; some failures may be resolved by fields from another image. |
identityConflict | Object with detected: boolean, mergeAllowed: boolean, and images: array. Each conflicting entry has imageId, reason and documentNumbers. The merger excludes identity fields from conflicting images but can keep safe document metadata and fields from unaffected images. Do not automatically accept a conflicting identity. |
Field sources, names and value types
For a selected field k, read result.fields[k], result.fieldConfidences[k], and result.fieldSources[k] together. Additional diagnostic properties can be present; do not require an exact fixed set of source keys.
fullName is the canonical surname/given-name representation; displayName is the human-readable country-dependent ordering, and nameOrder is surname_given or given_surname. displayName and nameOrder are added only when both surname and givenNames are available. Their source can be derived from multiple inputs.
Normalized dates use YYYY-MM-DD when evidence supports normalization. Country values normally use ISO 3166-1 alpha-3 codes; values such as XXX or UNK can reflect unknown MRZ nationality. Keep country and document-type handling extensible.
categories and licenceCategories are document-specific category strings, not guaranteed arrays. height, weight, eyeColor, hairColor, restrictions and endorsements retain document/source conventions; no universal metric conversion or code expansion is promised. permitValidUntil is the permit validity date and can differ from the card dateOfExpiry. passportNumber can identify the passport referenced by a visa, while documentNumber identifies the visa itself. nameSuffix is separate from titlePrefix.
| Property | Description |
|---|---|
fieldSources[k].value | The selected field value; normally matches fields[k]. |
fieldSources[k].source | Origin label such as mrz, spatial_label, aamva_pdf417, a country-specific extractor, image_selected, ocr_fields, name_display or country_name_order. This set is extensible. |
fieldSources[k].imageId | Source image UUID, or null for a derived value that cannot be attributed to one image. Join to images[] by imageId when present. |
fieldSources[k].confidence | Numeric selected-source confidence, used to build fieldConfidences[k]. Derived display-name confidence uses the weaker of the selected surname/given-name confidences. |
fieldSources[k] additional properties | Source-dependent evidence or diagnostics may accompany the core entry. Candidate alternatives belong to full-view OCR diagnostics; they are not guaranteed here. |
Per-image read summary: result.images[]
This schema is present inside both result and summary, independently of view. A status of done only means the processing job completed; inspect readSuccess for readability.
| Property | Description |
|---|---|
imageId / status | Image UUID and its queued | running | done | error lifecycle status. |
runtimeMs | Milliseconds from image creation to completion, or null until both timestamps exist. |
fieldCount / avgConfidence / mrzFound | OCR field count, average OCR confidence (0 if unavailable), and whether an MRZ has found: true. Average OCR confidence describes OCR blocks, not each selected field. |
readSuccess | boolean | null. true: current OCR passes read-quality checks; false: a read/processing failure is known; null: no OCR-based decision yet and no processing error. Partial OCR can already have a decision. |
readFailureReason | string | null. A read-quality code when unsuccessful, processing_error for a failed image without OCR, otherwise null. Not a captureFeedback code. |
error | Processing error text or null. A read-quality failure can have error: null because the job ran successfully but the document was unreadable. |
flags | Array of unique image-level operational/extractor/read-warning codes. Session flags may differ after cross-image resolution. |
extractorCoverage | object | null. { status, country, documentType }. Status values: matched, generic_sufficient, missing_extractor, possible_new_layout, undetermined, not_applicable. country/documentType may be null. |
captureFeedback | Image-level { status, items } with the same item schema as session feedback, but without the aggregate imageIds list. A missing-fields item can remain here even if another image resolves it in the session aggregate. |
Full-view OCR, classification and MRZ diagnostics
Full-view payload.images[i].ocr and detection expose the pipeline output. These are extensible diagnostics, and earlier or failed records may contain only some fields. Use result.fields as the selected integration result.
OCR bounding boxes belong to their source image coordinate space. They are not necessarily coordinates on the original upload; use sourceVariant, sourceWidth and sourceHeight and the matching processed/cropped image when available. Do not place retry or rotated-image boxes on a different image without a coordinate transform.
| Property | Description |
|---|---|
ocr.success / error / engine | OCR-service success boolean, optional error text and engine name. These do not replace session-level read-quality decisions. |
ocr.text / raw / avgConfidence | Recognized text string; OCR block array; average block confidence. Each raw block has text, confidence, optional box, and optional sourceVariant/sourceWidth/sourceHeight. |
ocr.fields / fieldCandidates | Per-image selected fields and candidate objects. Candidate entries normally include field, value, source, confidence and internal ranking/evidence properties. Alternatives can disagree with the session-selected result. |
ocr.document and result.documents[] entries | type: string; country: string | null; confidence: number; signals: string array; side: optional string | null. Unknown type is normally unknown. signals explain classification evidence and are extensible. Session-synthesized classifications can omit side. |
ocr.mrz and result.mrz[] entries | found: boolean; type: string | null (MRZ layout); lines: string array; fields: object; validCheckDigits: object mapping named checks to booleans; confidence: number. MRZ fields can differ from visible/merged fields. An empty check map means no recorded checks, not proof of validity. |
ocr.recognition | Pipeline diagnostics, including layers, fusion, machine-readable results, extractor coverage, retry information, timings or conflicts when available. Layer output and evidence formats may evolve. |
ocr.processing / qualityGate / logs | Optional preprocessing, quality-gate and OCR log data returned by the pipeline; response diagnostics are not processing log files. These can explain corrections or rejected input; availability depends on the processing path. |
detection | Document-detection/crop diagnostics or null, depending on the request and pipeline. This is distinct from ocr.document classification. |
How to interpret completion, review and failure
First check the HTTP status: a 4xx/5xx error envelope is not a normal session result. For a session payload, continue polling while isProcessing is true. After it becomes false, require an uploaded image and inspect the merged result, required fields for your workflow, confidence, requiresReview and identityConflict.
success: true + status: finished + result.status: failed is possible: the processing jobs completed without all failing technically, but none of the images passed the read-quality checks. partial_or_complete is also not a guarantee that every required field exists.
A corrected capture-feedback item is informational. Request recapture based on unresolved warning/error items and their suggestedAction. Do not infer recapture solely from a non-empty flags array.
Treat flags, review reasons, source labels and field names as extensible. The lists below describe current codes or examples, not a closed schema.
| Property | Description |
|---|---|
Current flags | too_slow (runtime threshold exceeded); missing_extractor; possible_new_layout; missing_date_of_birth; unknown_nationality_code. Extractor and read warnings can be resolved differently at session level. |
Review-reason examples | processing_error, identity_conflict, missing_name, missing_document_number, too_few_fields, weak_identity_evidence, mrz_name_unreliable, unknown_document, document_not_found, or a quality-gate reason. These are separate from captureFeedback.items[].code. |
No result yet | result and summary can be null. Read no fields until a snapshot exists; keep polling if isProcessing is true. A newly created empty session needs an image upload, not endless polling. |
No MRZ or optional fields | An empty mrz array or an absent optional field does not by itself invalidate a read. Some documents have no MRZ and different layouts expose different fields. |
Multiple images | One failed image can coexist with usable merged fields from another image. Preserve per-image feedback for diagnosis, and use the final session fields and review decisions for your workflow. |
Early processing response (summary view)
Complete illustrative GET response before a merged result is available. success is true although OCR has not produced fields yet.
{
"success": true,
"sessionId": "22222222-2222-4222-8222-222222222222",
"status": "processing",
"isProcessing": true,
"summary": null,
"result": null,
"images": [
{
"imageId": "11111111-1111-4111-8111-111111111111",
"status": "running",
"sourceName": "example-front.jpg",
"fieldCount": 0,
"avgConfidence": 0,
"mrzFound": false,
"hasExternalResult": false,
"externalResultUpdatedAt": null,
"error": null,
"updatedAt": "2026-09-05T10:00:00.000Z",
"completedAt": null
}
],
"createdAt": "2026-09-05T10:00:00.000Z",
"updatedAt": "2026-09-05T10:00:00.000Z",
"completedAt": null
}Completed merged result object
Complete illustrative value of result (and identically summary) for one successful image. The outer completed envelope has success: true, status: finished, isProcessing: false. These synthetic examples were generated with the backend response formatter and merger. They do not represent an actual scan or a runtime benchmark.
{
"status": "partial_or_complete",
"isComplete": true,
"imageCount": 1,
"successfulImageCount": 1,
"failedImageCount": 0,
"fieldCount": 8,
"runtime": {
"startedAt": "2026-09-05T10:00:00.000Z",
"finishedAt": "2026-09-05T10:00:02.400Z",
"durationMs": 2400
},
"fields": {
"documentType": "passport",
"documentCountry": "GBR",
"documentNumber": "000123456",
"surname": "SMITH",
"givenNames": "JANE",
"fullName": "SMITH JANE",
"dateOfBirth": "1990-04-12",
"dateOfExpiry": "2030-04-12",
"displayName": "JANE SMITH",
"nameOrder": "given_surname"
},
"fieldConfidences": {
"documentType": 0.97,
"documentCountry": 0.97,
"documentNumber": 0.97,
"surname": 0.97,
"givenNames": 0.97,
"fullName": 0.97,
"dateOfBirth": 0.97,
"dateOfExpiry": 0.97,
"displayName": 0.97,
"nameOrder": 0.97
},
"fieldSources": {
"documentType": {
"value": "passport",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"documentCountry": {
"value": "GBR",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"documentNumber": {
"value": "000123456",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"surname": {
"value": "SMITH",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"givenNames": {
"value": "JANE",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"fullName": {
"value": "SMITH JANE",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"dateOfBirth": {
"value": "1990-04-12",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"dateOfExpiry": {
"value": "2030-04-12",
"source": "ocr_fields",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"displayName": {
"value": "JANE SMITH",
"source": "name_display",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
},
"nameOrder": {
"value": "given_surname",
"source": "country_name_order",
"imageId": "11111111-1111-4111-8111-111111111111",
"confidence": 0.97
}
},
"documents": [
{
"type": "passport",
"country": "GBR",
"signals": [
"session-field-merge"
],
"confidence": 0.97
}
],
"mrz": [],
"images": [
{
"imageId": "11111111-1111-4111-8111-111111111111",
"status": "done",
"runtimeMs": 2400,
"flags": [],
"extractorCoverage": null,
"avgConfidence": 0.97,
"fieldCount": 8,
"mrzFound": false,
"captureFeedback": {
"status": "ok",
"items": []
},
"readSuccess": true,
"readFailureReason": null,
"error": null
}
],
"captureFeedback": {
"status": "ok",
"items": []
},
"flags": [],
"requiresReview": false,
"reviewReasons": [],
"identityConflict": {
"detected": false,
"mergeAllowed": true,
"images": []
}
}Merged Result
The merged result is available under summary.fields or result.fields in both views. These contain the same merged data.
| Property | Description |
|---|---|
status | Merged read state: empty, processing, partial_or_complete, or failed |
imageCount | Number of images currently in the session |
successfulImageCount | Images that produced a usable read |
failedImageCount | Images that did not produce a usable read |
fieldCount | Number of selected, non-derived fields |
runtime | Session-level startedAt, finishedAt, and durationMs |
fields | Selected normalized field values |
fieldConfidences | Final confidence per selected field (0–1) |
fieldSources | Source layer, image and confidence for each field |
documents | Session-selected and differing image classifications |
mrz | MRZ readings with found: true; inspect check digits for validity |
images | Per-image result summary |
captureFeedback | Structured capture/readability feedback and recapture guidance |
flags | Unique operational, extractor-coverage, and field-quality signals |
requiresReview | Whether reviewReasons contains a review condition |
reviewReasons | Session-level identity/read-quality review reasons |
identityConflict | Safe summary of conflicting document identities across images |
isComplete | Whether every image reached a terminal state |
Available field names
fields is extensible rather than a fixed schema. These names are currently produced by document classification, MRZ parsing, extraction, or final session merge.
| Category | Field names |
|---|---|
| Document metadata | documentType documentCountry documentCode issuingCountry issuingAuthority placeOfIssue documentSide issuingJurisdiction |
| Primary identifiers | documentNumber personalNumber identityNumber nationalIdNumber administrativeNumber passportNumber |
| Holder name | surname givenNames fullName titlePrefix alsoKnownAs nameSuffix |
| Derived display name | displayName nameOrder |
| Birth and related names | birthFamilyName birthName motherMaidenName motherName fatherName parentsGivenNames |
| Personal data | dateOfBirth placeOfBirth sex nationality countryOfResidence occupation countryOfBirth eyeColor hairColor height weight |
| Dates and validity | issueDate signatureDate registrationDate dateOfExpiry validityPeriod visibleDates permitValidUntil |
| Card and national IDs | cardNumber cardSerialNumber cardAccessNumber can civilId curp nationalRegisterNumber voterKey |
| Additional identifiers | supportNumber procedureNumber recordNumber registryNumber replacementDocumentNumber section auditNumber |
| Address | address postalCode city streetName streetType streetAddress houseNumber addressLine2 staircase floor door provinceOfResidence |
| Driving licence | categories documentClass conditions licenceVersion licenceCategories restrictions endorsements |
| Residence and annotations | permitType remarks |
| MRZ optional data | optionalData optionalData1 optionalData2 |
fieldConfidences and fieldSources, and ignore unknown names so new extractors remain backwards compatible.Normalized dates use YYYY-MM-DD when evidence is sufficient. Country fields normally use uppercase ISO 3166-1 alpha-3 codes. Keep identifiers as strings because leading zeros are significant. visibleDates is an array of unassigned normalized date candidates. displayName and nameOrder are derived and do not increase fieldCount.
Capture Feedback
result.captureFeedback reports capture and readability problems in a stable, machine-readable form. It is also present in summary.captureFeedback. Per-image details are available under result.images[].captureFeedback.
unresolved and has warning or error severity.Example
{
"status": "warning",
"items": [
{
"code": "image_blurry",
"severity": "warning",
"status": "unresolved",
"message": "The image may be too blurry to read reliably.",
"messageKey": "capture.image_blurry",
"suggestedAction": "Hold the camera steady, move closer, and take another photo.",
"evidence": {
"laplacianVariance": 189.77,
"avgConfidence": 0.699
},
"imageIds": ["3e3c7eff-aee5-47fd-aa1b-9f8d40af83ae"]
}
]
}Status values
| Status | Meaning |
|---|---|
ok | No capture issue was detected |
info | One or more issues were corrected automatically |
warning | Recognition completed, but recapture may improve or complete the result |
error | The image could not be read because of a fundamental capture problem |
Item properties
| Property | Description |
|---|---|
code | Stable issue code; use this for application logic |
severity | info, warning, or error |
status | corrected or unresolved |
message | Default English description |
messageKey | Stable localization key prefixed with capture. |
suggestedAction | English recapture guidance, or null when no action is needed |
evidence | Optional diagnostics; thresholds and evidence fields may evolve |
imageIds | Affected image IDs; present on aggregate session feedback |
Issue codes
| Property | Description |
|---|---|
image_blurry | The image is likely too blurred for a reliable read |
image_quality_insufficient | The document could not be read reliably, but no more specific capture problem was proven |
image_too_dark | The image is too dark to read |
image_too_bright | The image is overexposed or too bright to read |
no_document_visible | No readable document signal was detected |
document_unrecognized | A document is visible, but its country or document type could not be identified reliably. |
orientation_corrected | Rotation was detected and corrected automatically |
skew_corrected | Tilt or perspective was corrected automatically |
document_too_skewed | Excessive tilt or perspective remains unresolved |
document_partially_visible | A document edge or section may be outside the image |
document_data_page_required | A passport observation or reserved page was supplied instead of the photo and personal-data page |
missing_required_fields | Expected core fields could not be extracted |
Suggested actions
Default English values of suggestedAction are listed below. This field contains guidance text, not an action code. Automatically corrected issues return JSON null, so no user action is needed. Use the issue code for application logic and localization; do not match the English text.
| Property | Description |
|---|---|
image_blurry | Hold the camera steady, move closer, and take another photo. |
image_quality_insufficient | Retake the photo in sharp focus, with even lighting, and keep the whole document visible. |
image_too_dark | Retake the photo in brighter, even lighting. |
image_too_bright | Reduce glare and retake the photo in even lighting. |
no_document_visible | Place the whole document inside the frame and retake the photo. |
document_unrecognized | Retake the photo straight on in sharp, even light and include the whole document. |
orientation_corrected | null (no user action needed) |
skew_corrected | null (no user action needed) |
document_too_skewed | Photograph the document straight from above. |
document_partially_visible | Retake the photo with all document edges and corners visible. |
document_data_page_required | Upload the side or page containing the holder photo and personal data. |
missing_required_fields | Make sure the whole document is visible and upload the other side if applicable. |
When document_data_page_required is returned, ask the user to open the passport on the photo and personal-data page and capture that page instead. In multi-image sessions, aggregate missing_required_fields is suppressed when another image recovered all missing fields. Branch on code, status, and severity, not numeric evidence thresholds.
Statistics
Daily API stats
/v1/stats/todayReturns statistics for the current UTC day. The request itself is included in apiRequests.total.
Weekly API stats
/v1/stats/weekReturns statistics from Monday through the current day in UTC.
Monthly API stats
/v1/stats/monthReturns statistics from the first day of the current month through the current day in UTC.
Statistics response
All three endpoints use the same response shape. Country, document type, and failure-reason breakdowns contain at most the ten most frequent values in the selected period.
{
"period": "month",
"fromDate": "2026-08-01",
"toDate": "2026-08-29",
"apiRequests": {
"total": 18540
},
"sessions": {
"created": 4120,
"completed": 4055,
"withReadableResult": 3920,
"withoutReadableResult": 135
},
"images": {
"uploaded": 6890,
"processed": 6844,
"readSuccessful": 6530,
"readFailed": 314,
"processingErrors": 21,
"readSuccessRate": 95.41
},
"performance": {
"averageProcessingTimeMs": 2840
},
"quality": {
"averageFieldCount": 7.4,
"averageConfidence": 0.91
},
"failureReasons": [
{ "reason": "document_not_found", "count": 82 },
{ "reason": "required_fields_missing", "count": 64 }
],
"documentTypes": [
{
"documentType": "identity_card",
"count": 3560,
"readSuccessful": 3420,
"readFailed": 140
}
],
"countries": [
{
"country": "POL",
"count": 1240,
"readSuccessful": 1190,
"readFailed": 50
}
]
}Statistics field meanings
| Property | Description |
|---|---|
apiRequests.total | Authenticated HTTP requests in the period, including statistics requests |
sessions.created | API sessions created during the period |
sessions.completed | Sessions reaching a terminal state during the period |
sessions.withReadableResult | Completed sessions where at least one image passed the read-quality checks |
sessions.withoutReadableResult | Completed sessions where no image passed the read-quality checks |
images.uploaded | Images accepted by API sessions during the period |
images.processed | Image processing jobs completed during the period |
images.readSuccessful | Processed images that passed the read-quality checks |
images.readFailed | Processed images that did not pass read-quality checks, including processing errors |
images.processingErrors | The technical-processing-error subset of images.readFailed |
images.readSuccessRate | Percentage of processed images that passed read-quality checks |
performance.averageProcessingTimeMs | Average completed image-processing duration in milliseconds |
quality.averageFieldCount | Average extracted field count per processed image |
quality.averageConfidence | Average OCR confidence where a confidence sample was available |
failureReasons | Top 10 unsuccessful-read reasons |
documentTypes | Top 10 detected document types with read outcome counts |
countries | Top 10 detected issuing countries with read outcome counts |
Creation, upload, processing, and completion are counted on the UTC date when each event occurs. A session created before the selected period may therefore be counted as completed inside the period. Statistics are read from daily aggregates rather than scanning individual image records.
Session and uploaded-image counters are collected from the statistics-rollup deployment onward; historical sessions are not scanned or estimated automatically.
Error Responses
Unknown session 404 Not Found
{ "error": "Session not found" }A session created by one API key is not visible to another API key.
Invalid upload 400 Bad Request
{
"error": "Uploaded file is not a valid supported image",
"code": "INVALID_UPLOAD",
"requestId": "req-124"
}File too large 413 Payload Too Large
{
"error": "request file too large",
"code": "PAYLOAD_TOO_LARGE",
"requestId": "req-125"
}Session image limit 400 Bad Request
{
"error": "Session image limit reached (10)",
"code": "INVALID_UPLOAD",
"requestId": "req-126"
}JavaScript Example
Run this example on your backend with the front and back image buffers. Keep the API key secret. Use the partial callback to forward available fields to your UI; a late partial response is ignored once the final upload response arrives.
// Server-side example. Supply frontBuffer and backBuffer from your own backend.
// Keep the API key secret; configure client/proxy timeouts for at least 310 seconds.
const baseUrl = 'https://bck.idscanly.com/api/v1';
const headers = { 'x-api-key': process.env.IDSCANLY_API_KEY };
async function scanDocument(frontBuffer, backBuffer, onPartial = () => {}) {
// 1. Create one session for all sides and retakes of this document.
const created = await fetch(`${baseUrl}/sessions`, {
method: 'POST', headers,
}).then(readJson);
const sessionUrl = `${baseUrl}/sessions/${created.sessionId}`;
// 2. Upload the first photo immediately. The 202 response is an acknowledgement.
await upload(frontBuffer, 'front.jpg', false);
// Add any retakes or other sides here with wait=false, in this same session.
// 3. Upload the known final photo and wait for all queued/running images.
let finalResponseReceived = false;
const finalUpload = upload(backBuffer, 'back.jpg', true).then((snapshot) => {
finalResponseReceived = true;
return snapshot;
});
// 4. Read partial data once while the last upload is waiting.
// The GET may return no fields yet. Its failure does not cancel the upload.
void fetch(`${sessionUrl}?view=summary`, { headers }).then(readJson)
.then((snapshot) => {
if (!finalResponseReceived && snapshot.result) {
onPartial(snapshot.result.fields);
}
}).catch(() => { /* Optional partial preview unavailable. */ });
let session = await finalUpload;
// 5. After a wait timeout (202, waitTimedOut: true), poll as fallback.
while (session.isProcessing) {
await new Promise((resolve) => setTimeout(resolve, 2000));
session = await fetch(`${sessionUrl}?view=summary`, { headers }).then(readJson);
}
if (session.status === 'failed' || !session.result) {
throw new Error('No usable session result; review or recapture the document.');
}
return session;
function upload(buffer, filename, wait) {
const form = new FormData();
form.append('image', new Blob([buffer], { type: 'image/jpeg' }), filename);
return fetch(`${sessionUrl}/images?wait=${wait}&view=summary`, {
method: 'POST', headers, body: form,
}).then(readJson);
}
}
const session = await scanDocument(frontBuffer, backBuffer, (fields) => {
// Forward partial fields to your application's UI over your own secure channel.
});
// Before accepting session.result.fields, check required fields, confidence,
// requiresReview and identityConflict. A completed request can still need review.
async function readJson(response) {
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
return payload;
}For HEIC images, adapt the upload helper to use the original filename and MIME type:
form.append(
'image',
new Blob([heicBuffer], { type: 'image/heic' }),
'front.heic'
);Privacy Notes
Send images over encrypted HTTPS. IdScanly processes document images without retaining them or the extracted personal data, and does not write processing log files. Only broad, aggregate document statistics, such as country and type, are retained. Diagnostic fields in an API response describe processing results; they are not log files.
Identity document images and extracted fields are sensitive personal data. API responses include Cache-Control: no-store, but clients must also:
- •Avoid browser caching of responses
- •Encrypt data in transit and at rest
- •Avoid placing fields in URLs
- •Avoid logging full response bodies
- •Delete local temporary files promptly
- •Restrict access to diagnostic responses
- •Comply with applicable privacy and retention requirements
Ready to integrate?
Request an API key and start extracting structured data from identity documents.
Get an API key