Skip to Content
Extraction and verificationSessions and execution context

Sessions

A session associates workflow runs with a particular user. Use this API primitive to run document extraction and verification workflows for that user, share data between runs and track their progress.

For KYC or KYB, configure the document tasks and checks your process needs. The session holds the execution context; the workflows perform the document work. Sessions can also include related participants, such as a business’s representatives or beneficial owners.

Key Concepts

  • Session configurations: Reusable blueprints that define which workflows, bindings, verification settings, and defaults a session uses.
  • Sequential execution: Workflows run in order. The user must complete workflow 1 before starting workflow 2.
  • Participants: Sessions can include a subject plus related parties such as beneficial owners, controllers, directors, or representatives.
  • Cross-workflow data flow: Output from earlier workflows is available as input to later ones (e.g., validate POA name matches ID name).
  • Checks and risk: Sessions can include provider-backed check workflows and native risk scoring before final resolution.
  • Resolution: Sessions can be auto-resolved (approved/rejected) based on validation results, or manually resolved by a partner.
  • End-user access: Each session gets a unique access token that your frontend can turn into the collection URL for end-users.

Test and live environments

API keys select their own environment. A test key can create and access test configurations, sessions and workflow runs; a live key can access live resources. Create private workflow definitions and configurations in the same environment as the session. Both environments may use the same slugs. Public catalog definitions are shared; each run keeps the caller’s environment.

Dashboard JWT requests default to live. Use X-Korala-Mode: test, or the SDK’s korala.sessions.inMode('test') and korala.workflows.inMode('test') views, to select test mode. These views leave the original client unchanged. A header cannot override an API key’s mode.

Session links inherit the session’s environment. You cannot move an existing session between environments. Responses expose sandbox, and risk evidence imports require a subject in the same organization and environment.

Test mode isolates records and webhook routing. It does not mock OCR, model or check providers, or make their services free. Configure provider test credentials or mock providers before running a test workflow.

Session Configurations

Before creating sessions, set up a configuration that defines the session blueprint. Configurations are org-scoped and reusable.

import { KoralaClient } from '@korala/api-client'; const korala = new KoralaClient({ apiKeyId: 'your-api-key-id', apiSecret: 'your-api-secret', }); // Create a configuration const config = await korala.sessions.createConfiguration({ name: 'KYB Standard', slug: 'kyb-standard', description: 'Business onboarding with participant-aware verification', workflowBindings: [ { workflowSlug: 'id-document', participantRoles: ['representative'], outputKey: 'representative_id', }, { workflowSlug: 'proof-of-address', participantRoles: ['representative'], outputKey: 'representative_poa', }, { workflowSlug: 'pep-check', participantRoles: ['beneficial_owner'], outputKey: 'ubo_pep_check', }, ], subjectKind: 'business', isVerification: true, autoResolve: false, finalManualReviewRequired: true, defaultExpiresInMinutes: 1440, defaultRedirectUrl: 'https://yourapp.com/verification/complete', });

Configuration Options

OptionTypeDefaultDescription
namestringrequiredDisplay name
slugstringrequiredURL-safe identifier (unique per org)
workflowSlugsstring[]—Ordered list of workflow slugs for simple sequential sessions
workflowBindingsobject[]—Participant-aware workflow definitions with per-step context and output keys
subjectKind`individual \business`individual
isVerificationbooleantrueEnable resolution (approved/rejected)
autoResolvebooleantrueAuto-resolve based on validation results
finalManualReviewRequiredbooleanfalseForce partner review before final resolution
defaultExpiresInMinutesnumber1440Default session expiration
resubmissionMinimumWindowMinutesinteger1440Time a resubmission leaves the person at least before the session expires (0 to 43,200; 0 never extends). See Resubmission and expiry
defaultRedirectUrlstring—Default redirect after completion

Managing Configurations

// List all configurations const configs = await korala.sessions.listConfigurations(); // Get a specific configuration const config = await korala.sessions.getConfiguration('kyc-standard'); // Update a configuration await korala.sessions.updateConfiguration('kyc-standard', { defaultExpiresInMinutes: 2880, }); // Delete a configuration await korala.sessions.deleteConfiguration('kyc-standard');

Creating a Session

Each session starts from a configuration:

const session = await korala.sessions.create({ externalUserId: 'org_abc123', configurationSlug: 'kyb-standard', participants: [ { role: 'subject', displayName: 'Acme Corp' }, { role: 'beneficial_owner', displayName: 'Jane Smith' }, { role: 'representative', displayName: 'John Smith' }, ], context: { partnerName: 'Acme Corp', applicationType: 'merchant_onboarding', }, metadata: { applicationId: 'app_001', }, // Override config defaults if needed expiresInMinutes: 720, redirectUrl: 'https://yourapp.com/custom-redirect', }); // Share the access URL with the end-user const accessUrl = `https://yourapp.korala.ai/v/${session.accessToken}`;

Use the session detail endpoint when you need full case state, including participants, workflow instances, checks, and risk summaries.

Session Lifecycle

Session lifecycleCreated sessions move into progress. They can complete, pause for review, expire or fail. Reviewed sessions can then complete.StartFinish workflowsReview neededResolveTime expiresNo retries leftCreatedIn progressCompletedPending reviewExpiredFailed
Created sessions move into progress. They can complete, pause for review, expire or fail. Reviewed sessions can then complete.
StatusDescription
createdSession created, user hasn’t started yet
in_progressUser is completing workflows
pending_reviewA workflow is paused for human review
completedAll workflows finished without failures
expiredThe person did not finish before expiresAt (a session in review never expires)
failedA workflow failed with no retries remaining

Resolution

When all workflows complete, the session may auto-resolve if enabled in the configuration and nothing requires manual review:

  • Approved: All workflows passed validation
  • Rejected: One or more workflows failed validation

For KYB or other review-heavy flows, the session can move into pending_review until a partner makes the final decision.

Automatic decisions

A verification configuration with autoResolve on decides on its own:

  • Rejected only when a check you set to blockingMode: "block" returns a definite result: a clear face mismatch from aws_rekognition (below its mismatch threshold), a sanctions match from korala_screening, a Face Liveness confidence from aws_rekognition_liveness below the declineConfidence you set, or, on a duplicate_document risk step, an identity document already approved for another user in your organization. The public names korala_face_match and korala_liveness are the same checks: a step configured with either can be set to block and declines exactly like aws_rekognition or aws_rekognition_liveness. Every other check, and every one of these set to manual_review, goes to review instead. Nothing blocks by default.
  • Review whenever a check is not clear, a result is uncertain, or any evidence is missing or failed. Missing evidence holds the session in review even when another check would decline it: a check step that recorded no check, a check that errored, or a sanctions screening that was incomplete (a stale or attention-flagged list, entries dropped from a list, capped candidates). A sanctions match declines only on a complete screening. A face mismatch is review, never a decline, when a capture problem (a capture.* code: an unreadable, expired, unsupported or one-sided document) was found anywhere in the session’s current attempts, in any workflow: for example, a proof-of-address upload that could not be read holds a face mismatch in the identity workflow, since the capture may be what failed. A PEP and adverse media never decline, nor does liveness without your own decline threshold.
  • Approved when every check is clear and nothing needs review, with or without a validate step.

Saving a workflow with block on a check that can never decline (document authenticity, adverse media, a vision-model face match or liveness check, the local liveness model, your own HTTP check) is refused.

A korala_liveness step’s thresholds are now checked when the workflow is saved, as aws_rekognition_liveness ones always were: an existing workflow whose korala_liveness thresholds are invalid gets a 400 on its next update until they are fixed.

Liveness declines only below a threshold you choose. Korala sets no default and has not measured where genuine people and attacks separate for your users. Below clearConfidence (90 unless you set it) a session goes to review; below declineConfidence it is declined. A threshold set too high declines genuine people: someone in bad light or on an old camera scores low too. Start low, for example 10, and raise it only on your own reviewed results.

{ "type": "check", "config": { "kind": "active_liveness", "provider": "aws_rekognition_liveness", "blockingMode": "block", "providerConfig": { "clearConfidence": 90, "declineConfidence": 10 } } }

declineConfidence must be below clearConfidence; without it, block is refused.

Each session carries a decision:

{ "outcome": "rejected", "decidedBy": "auto", "decidedAt": "2026-10-12T09:30:00.000Z", "rulesVersion": "korala-decisions-v3", "reasonCodesVersion": "korala-reasons-v1", "codes": ["biometric.face_mismatch"], "decidingCheckIds": ["8b0d…"], "findings": [ { "code": "biometric.face_mismatch", "effect": "decline", "checkId": "8b0d…" } ], "attempts": [ { "sessionWorkflowId": "c41e…", "workflowRunId": "5f2a…", "attempt": 1 } ] }

decidedBy is auto, reviewer (someone in the dashboard) or partner (your integration through the API). codes are Korala’s reason codes, in the families document, biometric, duplicate, screening, capture and data: the decline codes of a rejection (plus duplicate.identity_changed_between_attempts where the person changed between attempts, which never declines itself; its finding stays review), or the codes holding a session in review. Route on them rather than on resolutionReason, which is for people. The record holds codes and ids only, never personal data, and is included in session webhooks.

Partners can override the resolution at any time before the session expires:

await korala.sessions.resolve(sessionId, { resolution: 'approved', reason: 'Manually verified after review', });

An expired session stays expired: resolving it answers 409 with reason: "session_expired" (see Resubmission and expiry for what counts as expired). A session in pending_review never expires, so you can decide it after its deadline.

Listing Sessions

const sessions = await korala.sessions.list({ externalUserId: 'usr_abc123', status: 'completed', page: 1, limit: 10, });

Session Webhooks

Subscribe to these events in your webhook configuration:

EventDescription
session_completedAll workflows in the session finished. status says whether it is completed or waiting in pending_review
session_resolvedA resolution was set or changed, automatically (resolvedBy: "auto") or by your team. With resolution: null and decision: null, a resubmission withdrew the earlier decision
session_review_requiredThe session entered pending_review: a workflow paused for review, or its results need a decision from you
session_resubmission_requestedKorala asked the person to redo one or more workflows because of a capture problem; resubmission.workflows lists each with its attempts left, resubmission.sessionWorkflowId is the earliest and resubmission.attemptsRemaining the fewest left; expiresAt is the session’s expiry after the resubmission extended it, or null
session_expiredThe session expired: the person did not finish (created or in_progress) by expiresAt, or you expired it. A session in review never expires
session_failedSession failed (no retries remaining)
session_workflow_completedIndividual workflow within session completed
session_workflow_failedIndividual workflow within session failed

Every session event is recorded in the same database transaction as the change it reports, then sent within a few seconds. Each change is announced once; a later, different outcome is a new event. For example, an automatic approval that a late check sends back to review is followed by session_review_required with resolution: null, and a later automatic rejection by a second session_resolved. The same outcome judged again with different decision.codes is announced too: session_resolved for a resolved session, session_review_required for one in review.

session_completed, session_resolved, session_review_required and session_workflow_completed carry the session’s sessionId, externalUserId, status, resolution, resolvedBy, riskScore, riskLevel, sandbox and decision (the decision record: codes, ids and versions), and no personal data: fetch the session for its results, including its riskSummary, which session_resolved no longer carries. A resolution set by your team also carries the reason you gave and overridden. session_workflow_completed also carries sessionWorkflowId and workflowSlug, the session workflow that finished.

session_expired carries sessionId and externalUserId. session_workflow_failed carries sessionId, externalUserId, errorMessage, workflowSlug, attemptsRemaining and expiresAt (the session’s expiry, extended when you are asked to retry the step, or null for a session with no expiry); session_failed carries sessionId, externalUserId, errorMessage and failedWorkflow.

errorMessage in session_failed and session_workflow_failed, and reason in session_resolved, can be free text your own reviewers wrote when they rejected a step or resolved the session.

Every session event’s data carries version, a number that grows with each change Korala announces for that session. Events of one change share it. Numbers may skip, and an event without version is older than any event with one. The session itself carries the same number as eventVersion.

Delivery is at least once, and events can arrive out of order. Verify the signature and deduplicate on eventId. Compare version with the newest you have seen for the session, or with the session’s eventVersion: the higher one wins, and an event with a lower version is older news. Read the session before acting on it.

Resubmission

Automatic

When the only problems in the session are ones the person can fix, Korala asks them to redo the workflow as soon as it finishes or its capture step fails, without waiting for the rest of the session:

CodeWhen
capture.document_expiredThe document has expired
capture.document_unsupportedThe document or file type is not accepted
capture.image_unreadableThe document could not be read
capture.side_missingA side of the document is missing
capture.file_too_largeThe file is larger than the step accepts
capture.document_no_faceNo face was found on the identity document photo
capture.selfie_no_faceNo face was found in the selfie
capture.selfie_multiple_facesMore than one face was found in the selfie
capture.selfie_too_darkThe selfie face is darker than the step’s minSelfieBrightness
capture.selfie_blurryThe selfie face is less sharp than the step’s minSelfieSharpness

The selfie and portrait codes come only from Amazon Rekognition’s own face detection on an aws_rekognition face match whose similarity was uncertain or could not be computed. A clear match is never inspected, a clear mismatch stays biometric.face_mismatch, and an uncertain similarity with one usable face stays biometric.face_uncertain. The selfie is asked for in the workflow that captured it; a document with no face asks for the identity document again, and the face match that read it. These codes never decline, even on a face match set to block. minSelfieBrightness and minSelfieSharpness (0 to 100, in the step’s providerConfig) have no default; without them only the face counts apply. An unknown key, or a value outside 0 to 100, in an aws_rekognition face match’s providerConfig is refused with 400 when the workflow is saved.

A biometric verdict that was not clear on an earlier attempt carries forward: a face mismatch (declined or not), an uncertain face match, or Face Liveness below its clear threshold or a spoof. A result from your own HTTP check or a vision model that is not clear, an error included, carries forward too, whatever its metadata says. An error from Korala’s own face comparison or liveness check, or one of their results that compared nothing (no face found, an unusable image, a liveness session missing, failed, expired or unfinished), does not carry. A result from any participant of the session holds the whole session.

The person may still retake, but a later clear result goes to review with biometric.earlier_attempt_not_clear (in codes, the earlier check in decidingCheckIds) instead of approving; a definite decline still declines. What clears it is the approval in force, yours or a reviewer’s in the dashboard, and only for results from before that approval. Nothing else does: not your resubmission of any workflow, not a rejection (a rejection after an approval brings back what that approval had cleared), and not reopening with reopen: true (a result written after the approval, which nobody has seen, still holds). A retake for more than one face, darkness or blur happens only beside an uncertain similarity, so it always ends in review or a decline; a retake for no face can still approve. A resubmit decision’s codes are what is asked for; its findings can also hold this review finding. These rules are rulesVersion: "korala-decisions-v3".

The workflow goes to resubmit_required with these codes in resubmissionReasons, the session stays in_progress, its decision has outcome: "resubmit", and you receive session_resubmission_requested (and no session_resolved: nothing was decided or withdrawn). The person sees why on the session link and tries again there. If anything else in the session needs review (a PEP, an uncertain face match, a risk finding, a validation result, missing evidence), the session goes to review instead, and a definite decline still declines.

Every workflow with a capture problem of its own is asked for again, with its own reasons, together with the later ones that read its result (see “On request” below for what reading means); the rest keep their result. A failure with no reason the person can act on is retried with a plain “We couldn’t finish this step. Please try it again.”

The decision record and resubmissionReasons stay while the person redoes the workflow, as the record of why they were asked; the next decision replaces the record, and the workflow’s next completion clears its reasons.

A file Korala could not read back from its own storage is retried on Korala’s side. If it still cannot be read, the session goes to pending_review with data.evidence_unavailable: the attempt is not counted against the person and the session does not fail, so your team can resubmit it. A file replaced with more bytes after it was confirmed is the person’s file, and is refused as capture.file_too_large.

Each workflow allows maxAttempts attempts (3 unless the configuration’s workflow binding sets 1 to 20). At the limit the session goes to pending_review at once, even if later workflows remain, with data.attempts_exhausted beside every finding: never a decline, and never failed for a capture problem. Automatic resubmission needs a session configuration that is a verification flow with autoResolve on; otherwise, and for sessions created without a configuration, you decide.

Resubmission and expiry

A resubmission moves the session’s expiresAt out so the person has time to redo the step: to now plus the configuration’s resubmissionMinimumWindowMinutes (a day unless you set it), or leaves it where it is when that is later. It never brings an expiry closer. This applies to every resubmission, Korala’s automatic one and yours, a reopen included, and to every release of a session from pending_review back to in_progress with workflows still to run: a review step approved, or an async check (such as a face match) that finishes. A session with no expiry keeps none. The window is read from the configuration each time, so changing it also changes what sessions already under way get the next time their work resumes.

A session has expired when its status is expired, or when the person had not finished (created, in_progress) by expiresAt. A session in pending_review never expires: the person has finished, and only the review is late. An expired session stays expired. Resolving or resubmitting it answers 409 with reason: "session_expired" (reopen does not change that). Create a new session to ask the person again. With the API client, read error.reason (or error.code) on the KoralaError.

A session the person finished (in review, completed, or decided by your team, your integration or Korala, at any time) can be resubmitted or reopened after expiresAt, and it gets the window from now. A review step in a workflow (POST /workflows/runs/:id/review) can likewise be approved or rejected after the deadline while the session is in review; it answers 409 with reason: "session_expired" only once the session has expired. An approval that puts the session back in progress (a step that is not the workflow’s last, or the last one with other workflows still to run) moves expiresAt out by the window, as a resubmission does, so the remaining steps run. No webhook announces it; read the session.

With a window of 0 nothing is extended, so past expiresAt the person would have no time at all. A resubmit, a reopen, or a review approval that would put the session back in progress then answers the same 409 with reason: "session_expired", and nothing changes. An approval of a workflow’s last step is refused only when Korala can tell beforehand that it would put the session back in progress: with a check that is not clear on the session, it may ask for a redo instead, so the approval is taken. A review rejection is still taken: when the workflow has attempts left, the person is asked again with no extra time, so the session expires. A session Korala itself puts back in progress (an async check that finishes, a retry) keeps its deadline. Past expiresAt it is then marked expired within about a minute, and session_expired is sent.

A session whose person never finishes (created or in_progress past expiresAt) is marked expired on its own within about a minute of its deadline, whether or not anyone opens its link, and session_expired is sent once. Lists show it as expired from then on.

session_resubmission_requested carries expiresAt: the session’s expiry after the extension, as an ISO 8601 time, or null for a session with no expiry. It can be in the past when Korala judged the step after the person’s deadline: the session has expired, and the person will not be asked again. session_workflow_failed carries expiresAt too, so a failed step you are asked to retry (where you decide) shows its extended expiry. The session itself (GET /sessions/:id, or the response of your resubmit request) shows the expiry as well.

On request

If a workflow fails or the user must redo it, request resubmission:

await korala.sessions.resubmit(sessionId, sessionWorkflowId);

This resets the workflow and the later workflows that read its result: a face match that compares the selfie with the ID’s portrait, a check that compares the name on a proof of address with the ID, or any step that reads the whole session (validation, review, risk, screening, your own HTTP check). Later workflows that do not read it keep their result, so redoing a proof of address that comes after the identity steps does not make the person redo their identity verification. A redo of the identity document does reset a later proof of address whose validation or name check reads the identity: that proof of address was checked against it. When Korala cannot tell whether a workflow reads it (including a workflow whose steps or schemas you changed after it ran; renaming it does not count), it resets it. Reset workflows get their attempts back (attemptCount returns to 0): being reset because an earlier step changed is not the person’s try at them.

The decision is made again from the new attempt, and risk summaries are recomputed from the updated evidence.

Attempts

The organisation’s session responses (GET, create, resolve, resubmit) list every attempt at each workflow in workflows[].attempts, ordered by attemptNumber and then any attempt not yet numbered by startedAt, the current one included:

{ "workflowRunId": "5f2a…", "attemptNumber": 1, "status": "completed", "startedAt": "2026-10-12T09:20:00.000Z", "completedAt": "2026-10-12T09:21:10.000Z", "resubmissionReasons": ["capture.image_unreadable"], "documentChoice": { "country": "DE", "type": "passport" }, "checkIds": ["8b0d…"] }

checkIds join to session.checks; an earlier attempt’s checks are the ones with supersededAt set. resubmissionReasons is why Korala asked for that attempt again, and stays on the attempt after the workflow’s own resubmissionReasons clears. attemptNumber follows the order the runs were claimed and is never rewritten (decisions cite it as decision.attempts[].attempt). A run an older version of Korala started during an upgrade is numbered when the next run of that workflow starts or the session is next decided, so it can be numbered after a newer run and the numbers need not follow startedAt; sort by startedAt if you need start order. Until then it has attemptNumber: null: Korala never shows a number that would change later. If an older version made the session’s last decision, earlier runs can stay unnumbered, and that decision cites the older version’s attempt counter. A decision cites the stored number; in the one case where a run was never numbered (it should not happen), it cites attempt: null. When Korala decides automatically, it then records data.attempt_unnumbered: an approval or a resubmission goes to review instead, with the code in codes; a review gains the code in codes; beside a definite decline the code sits in findings, not in codes, and the decline stands. Where Korala makes no automatic decision (not a verification flow, or autoResolve off with nothing to review), there is no decision record at all. A reviewer’s or your own decision stands and simply cites null. The number keeps counting when a workflow is reset because an earlier one changed, while its attemptCount (the cap) starts again. The person’s session link never shows attempts.

On a new attempt the person chooses their document again, among the types the binding’s documentTypes accepts: an unreadable passport can be followed by an ID card.

A different person between attempts goes to review. When a later attempt’s identity document shows a different person from an earlier one, the session goes to review with duplicate.identity_changed_between_attempts. The switch itself never declines; a definite decline from a check set to block still declines, with the switch recorded among decision.codes. It stops an automatic resubmission. A switch needs two disagreements, and any of three signals is enough:

  • name_and_date_of_birth: the name and the date of birth both differ;
  • document_number: another document (number and issuing country) whose name differs while the date of birth is not shown to agree (unread on either);
  • document_and_date_of_birth: another document with another date of birth, even under the same name (two people of one name).

A renewed passport or an ID card for the same person is not a switch, and neither is the same document re-read with one misread field. A date of birth misread on a renewed document does go to review. A name changed on a renewal with the same date of birth is unknown. The decision records identityAcrossAttempts: { "status": "same" | "changed" | "unknown", "signals": [...] }, where unknown means the attempts could not be compared (a field either could not read, or a name that changed alone), never that it was the same person; it does not hold the session. Attempts from before this was recorded have no resubmissionReasons.

A session your team or your integration already decided keeps its decision: resubmitting it answers 409 unless you reopen it explicitly (a reviewer in the dashboard can reopen it too, after confirming):

await korala.sessions.resubmit(sessionId, sessionWorkflowId, { reopen: true });

A resubmission that withdraws a decision sends exactly one session_resolved with resolution: null, decision: null and status: "in_progress", in the same transaction: treat the earlier decision as no longer standing and wait for the next one. That covers a decision by your team or integration (reopened with reopen: true), an automatic approval or rejection, and a session waiting in pending_review, whose review decision is withdrawn too even though it had no resolution.

Pass the Korala risk subject ID alongside your existing customer reference:

const session = await client.sessions.create({ configurationSlug: 'kyc-standard', externalUserId: 'customer-123', subjectId: customer.id, }); const history = await client.sessions.list({ subjectId: customer.id });

The subject must already exist in the same organization and environment. Its external reference must equal externalUserId, and its type must match the session configuration. Korala rejects mismatches; it never matches customers by name or email. Several sessions can link to one subject. The optional link is fixed at creation and returned in session list/detail responses. Existing sessions remain unlinked. Linking does not import evidence, run an assessment, or approve the customer.

Record the journey that prompted verification

Alice joins purchase B and needs to repeat KYC. Link her new session to that registered context so you can find the verifications started for B:

const session = await client.sessions.create({ configurationSlug: 'kyc-standard', externalUserId: 'customer-123', subjectId: customer.id, originContextId: purchaseB.id, }); const history = await client.sessions.list({ originContextId: purchaseB.id });

Register the context and add the customer as a participant first; see Risk contexts. The customer, context and session must belong to the same organization and environment. Without a linked customer or context membership, Korala rejects the request. Session list and detail responses include originContextId, or null when no origin was linked. The link is fixed at creation; putting a context ID in metadata or session context does not create it.

Origin records why a verification started. It does not make all its results exclusive to that purchase. General policies can still consume reusable native verification evidence through their configured bindings. Recording the origin does not approve Alice, run an assessment, or enable context-scoped policy evaluation. Context evaluation and policy composition are not available yet.

Last updated on