TruvaLI API Documentation

Integration interface for fraud prevention, AML monitoring and customer onboarding (KYC) services.

Overview

All endpoints are served under the /api/ prefix and speak JSON (except document upload endpoints, which use multipart/form-data). Request bodies must be UTF-8 encoded.

Endpoint list

Endpoint Method Auth Description
/api/merchant/auth/token/ POST none Obtain access token
/api/merchant/auth/check/ GET Bearer Token validity check
/api/customer/upsert/ POST Bearer Create/update customer
/api/transaction/financial/upsert/ POST Bearer Financial transaction reporting
/api/transaction/non-financial/upsert/ POST Bearer Non-financial event reporting
/api/document/create-session/ POST Bearer Open KYC session
/api/document/upload_document/ POST Session token Upload document (end user)
/api/document/take_snapshot/ POST Session token Take snapshot (end user)
/api/document/end-kyc-session/ POST Agent token End the interview
Two separate authentication mechanisms

Server-to-server calls use a Bearer token (JWT). KYC calls made from the end user's browser use a signed session token instead. Your API key must never be sent to the end user's device.

Authentication

POST /api/merchant/auth/token/ no authentication required

You obtain an access token using your API key and secret key.

Request

{
  "api_key": "05b190fb-c354-4431-bf04-450cf77e0306",
  "api_secret": "9d4f2a11-77c3-4b2e-9a51-1f0b8c3d7e42"
}

Response | 200

{
  "status": true,
  "message": "Login successful.",
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}

Usage in subsequent requests

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
IP restriction is required

The allowed IP list on your credentials cannot be left empty. An empty list means "allow no IP" and login attempts are rejected with 403. Use * to allow all IPs; in production, we recommend entering your servers' static IPs.

Errors

Status Message Reason
400 Request body is not valid JSON. Body could not be parsed
400 Request body must be a JSON object… Body is not an object (list/string was sent)
400 Missing required field(s): api_key… Missing field
401 Invalid credentials provided. Key or secret key is incorrect
403 This credential is not active. Credential has been deactivated
403 Access from this IP address is not allowed. IP is not in the allow list

Token check

GET /api/merchant/auth/check/ Bearer

Tests whether the token you have is still valid. Only callable via GET; a POST request returns 403 due to CSRF protection.

{
  "status": true,
  "message": "Token is valid for Example Customer.",
  "data": {
    "client": "Example Customer",
    "session_id": "8f2c1a90-3b7e-4d55-9c21-6a4b8e0d1f33"
  }
}

Response format

All JSON endpoints use the same envelope:

{
  "status":  true | false,     // whether the request as a whole succeeded
  "message": "a human-readable summary",
  "data":    { ... }           // endpoint-specific content
}
The status field does not indicate record-level success

In batch endpoints, status: true only means "the request was processed". The fate of individual records is in data.results; even if a record is rejected, the top-level status stays true.

Error model

There are two different error levels, and distinguishing between them matters for integration.

1. Request level: the entire request is rejected

If the body is malformed, no record is processed and 4xx is returned. This points to a programming error on the caller's side.

Status When
400 Body is not valid JSON
400 Body is not an object or array
400 Array is empty: Payload contains no records.
400 Array contains a non-object element: Record at index 0 must be a JSON object, got str.
401 Bearer token is missing, invalid, or expired
413 More than 1000 records were sent

2. Record level: only that record is skipped

If the body format is correct but a record's content is invalid, only that record is dropped, the others continue to be processed, and the request returns 200. The skipped record is flagged with action: "skipped" and error.

{
  "status": true,
  "message": "Processed 3 financial event record(s).",
  "data": {
    "results": [
      { "transaction_ref": "TX-1", "id": "…", "action": "created",  "score": 0,   "decision": "ACCEPT" },
      { "transaction_ref": "TX-2", "id": null, "action": "skipped",
        "error": "'amount' cannot be negative (got -1.00); use io_type to express direction.",
        "score": 100.0, "decision": "REJECT" },
      { "transaction_ref": "TX-3", "id": "…", "action": "created",  "score": 0,   "decision": "ACCEPT" }
    ]
  }
}

3. Warnings: the record is processed but something was corrected

Correctable deviations do not drop the record; the value is normalized and reported via warnings. We recommend monitoring these warnings and fixing them at the source.

{
  "transaction_ref": "TX-4",
  "action": "created",
  "warnings": [
    "'currency' was normalised to upper case ('usd' -> 'USD').",
    "'amount' was rounded to 2 decimal places (1.129 -> 1.13)."
  ]
}
Fields not in the model are unrestricted

You can send any field outside the defined ones. These are not rejected, do not produce a warning, and are stored in the record's raw field, making them available to the rule engine. This is designed so you can write rules based on your own business fields.

Batch requests

All upsert endpoints accept either a single object or an array of objects. Up to 1000 records can be sent per request.

// single record
{ "customer_ref": "C-1001", "first_name": "Jane" }

// batch
[
  { "customer_ref": "C-1001", "first_name": "Jane" },
  { "customer_ref": "C-1002", "first_name": "John" }
]

In both cases, the response returns an array inside data.results, and the order matches the order you sent.

Customer upsert

POST /api/customer/upsert/ Bearer

Creates or updates a customer record. The record is matched by customer_ref; a second call with the same reference performs an update. After it is saved, the customer is run through the rule engine and a risk score is calculated.

Fields

Field Type Constraint Description
customer_ref required text max 48 Your unique customer identifier
customer_type number 1 or 2 1 = Individual (default), 2 = Corporate
first_name text max 100
last_name text max 100
gender text male, female, other, unknown Case differences are normalized
birth_date date YYYY-MM-DD Used in email risk scoring
identity_number text max 100 Identity number
passport_number text max 100
passport_issuing_country text 3 letters ISO 3166-1 alpha-3
nationality text 3 letters ISO 3166-1 alpha-3, e.g. TUR
email text max 254 A quality score is calculated; an invalid format is not rejected, only flagged
phone text max 100
country text max 100
occupation text max 100 Occupation
ip_address text IPv4 / IPv6 For geolocation and IP risk score
status text max 24 Your customer status
registration_date date-time ISO-8601 The moment the customer registered with you
email_verified_at date-time ISO-8601
phone_verified_at date-time ISO-8601
migration true/false Historical data migration: skips external service calls (email, IP, sanctions)

Corporate customer fields

Available in addition when you send customer_type: 2:

Field Constraint Description
company_name max 255 Trade name
legal_form max 50 Legal structure (e.g. joint-stock company, limited liability company …)
tax_number max 100 Tax number
tax_office max 100 Tax office
sector max 150 Industry sector
incorporation_date YYYY-MM-DD Incorporation date
country_of_origin 3 letters ISO 3166-1 alpha-3
registry_number max 100 Trade registry number
lei_code max 20 Legal Entity Identifier
stock_ticker_symbol max 20 Stock ticker symbol
website max 255
founders array Founders / partners

Example request

POST /api/customer/upsert/
Authorization: Bearer <token>
Content-Type: application/json

{
  "customer_ref": "C-1001",
  "customer_type": 1,
  "first_name": "Jane",
  "last_name": "Smith",
  "gender": "female",
  "birth_date": "1990-04-17",
  "identity_number": "10000000146",
  "nationality": "TUR",
  "email": "[email protected]",
  "phone": "+905301234567",
  "ip_address": "88.230.14.7",
  "registration_date": "2026-01-15T09:30:00Z",

  "loyalty_tier": "gold",
  "internal_segment": "vip"
}

The last two fields are not defined in the model; they are not rejected, they are stored in the record's raw field and can be used when writing rules.

Example response | 200

{
  "status": true,
  "message": "Processed 1 customer record(s).",
  "data": {
    "results": [
      {
        "customer_ref": "C-1001",
        "id": "3f1c8a20-9d44-4e77-b1a2-77c9e0b34512",
        "action": "created",
        "score": 25.0,
        "decision": "ACCEPT",
        "sanction_score": 0,
        "sanction_topic": [],
        "ip_address_score": 12.5,
        "email_risk_score": 34.0,
        "warnings": []
      }
    ]
  }
}
Field Description
action created, updated, or skipped
score Risk score produced by the rule engine (0–100)
decision ACCEPT, REVIEW, or REJECT
sanction_score / sanction_topic Result of the sanctions list screening
email_risk_score Email quality/risk score (higher = riskier)
warnings List of fields that were normalized

Financial transaction upsert

POST /api/transaction/financial/upsert/ Bearer

Reports transactions that involve a movement of funds. The record is matched by transaction_ref; a second call with the same reference performs an update.

Prerequisites

The customer sent via customer_ref must already exist. event_slug must be an event defined in the admin panel, active, and of type Financial. If either is not satisfied, the record is skipped.

Fields

Field Type Constraint Description
customer_ref required text at most 48 Reference of an existing customer
event_slug required text Defined financial event code, e.g. deposit
amount required decimal > 0, at most 9999999999.99 Rounded to 2 decimal places. Cannot be negative; direction is expressed via io_type
currency required text 3 letters ISO-4217, converted to uppercase
transaction_ref required text at most 64 Deduplication key. Must be unique for each transaction
io_type required number 0 or 1 0 = Credit, 1 = Debit
event_date required date-time ISO-8601 The moment the transaction occurred
payment_method text at most 16 e.g. credit_card, wallet
fee_amount decimal ≥ 0 Transaction fee
fee_currency text 3 letters
source_provider text at most 64 Sending institution
source_ref text at most 64 Sender reference
source_address text at most 64 IBAN, wallet address, etc.
destination_provider text at most 64 Receiving institution
destination_ref text at most 64 Recipient reference
destination_address text at most 64 IBAN, wallet address, etc.
status text at most 16 Defaults to initiated
event_description text at most 128 Free-form description
session_id text at most 128 End user's session ID
device_id text at most 128 Device fingerprint
ip_address text IPv4 / IPv6 End user's IP address
channel number 0–4 See constant values
user_agent text Browser information
geo object Location data
metadata object Free-form additional data
Seven fields are required

A financial transaction is not recorded without customer_ref, event_slug, amount, currency, transaction_ref, io_type and event_date; the record is returned as skipped.

Although the last three have a technical default, leaving them at their default is a silent data error: without transaction_ref, a network retry creates a second transaction record; if io_type is wrong, debit/credit totals silently break; if event_date is wrong, time-windowed rules ("so many transactions in so many hours") silently break.

Example request

POST /api/transaction/financial/upsert/
Authorization: Bearer <token>
Content-Type: application/json

{
  "customer_ref": "C-1001",
  "event_slug": "deposit",
  "transaction_ref": "TX-2026-0007781",
  "amount": "15750.00",
  "currency": "TRY",
  "io_type": 0,
  "payment_method": "credit_card",
  "event_date": "2026-08-07T11:42:13Z",
  "source_address": "TR330006100519786457841326",
  "ip_address": "88.230.14.7",
  "channel": 1,
  "metadata": { "campaign": "summer26" }
}

Example response | 200

{
  "status": true,
  "message": "Processed 1 financial event record(s).",
  "data": {
    "results": [
      {
        "transaction_ref": "TX-2026-0007781",
        "id": "6b708204-81d2-49e2-a808-cbc9beae549b",
        "action": "created",
        "score": 40.0,
        "decision": "REVIEW",
        "rule_check": true,
        "log_id": "b1e7…",
        "warnings": []
      }
    ]
  }
}
rule_check field

Indicates whether the rule engine ran for this record. If event_date is older than 1 day, the rule engine is not run (so historical data migration doesn't generate live alerts) and this field returns false.

Non-financial transaction upsert

POST /api/transaction/non-financial/upsert/ Bearer

Reports events that do not involve a movement of funds, such as login attempts, password changes, IBAN registration, or profile updates.

The field set is the same as the financial endpoint's; there are no amount- or currency-related fields. If you send these fields anyway, they are not rejected; they are written to the record's raw field but not processed.

Field Constraint Description
customer_ref required at most 48 Reference of an existing customer
event_slug required Defined event code of type Non-financial or Onboarding
transaction_ref at most 64 Deduplication key. Optional on this endpoint; if not sent, the record cannot be deduplicated and you will receive a warning in the response
io_type not used Not present on this endpoint; if sent, it is only stored in raw
event_date required ISO-8601 The moment the event occurred
event_description at most 128 Free-form description
status at most 16 Defaults to initiated
session_id, device_id, ip_address, channel, user_agent, geo, metadata Same as the financial endpoint

Example request

POST /api/transaction/non-financial/upsert/
Authorization: Bearer <token>
Content-Type: application/json

{
  "customer_ref": "C-1001",
  "event_slug": "failed_login",
  "transaction_ref": "EVT-2026-0004412",
  "event_date": "2026-08-07T11:40:02Z",
  "event_description": "Incorrect password, attempt 3",
  "ip_address": "185.66.12.9",
  "device_id": "a91f3c7d20e4",
  "channel": 3
}
Three fields are required

No record is created without customer_ref, event_slug and event_date. Although event_date has a technical default (the moment of the request), leaving the moment the event occurred at its default is a silent data error: in case of queue delay or bulk submission, the request time and the event time can diverge by hours, and time-windowed rules ("so many attempts in so much time") run in the wrong window.

transaction_ref, however, is optional on this endpoint; that is the difference from the financial endpoint.

Event type must match the endpoint

You cannot send a non-financial event_slug to the financial endpoint (or vice versa). Such a record is skipped with the following error: Event 'login' has type 0 (Onboarding Event), which this endpoint does not accept. Accepted types: 1 (Financial Event).

Document verification (KYC) | Flow overview

The customer onboarding flow consists of five steps:

  1. Create session (server-to-server). Your platform calls create-session with a Bearer token and receives a session token (endpoint_token).
  2. Redirect to the end user. You pass the session token to the end user's browser. From this point on, document upload calls are made with this token; your API key never reaches the device.
  3. Document upload. The end user uploads the documents required by the flow using upload_document.
  4. Video call (if the flow requires it). An agent joins the call, and a snapshot is captured if needed.
  5. Resolution. The session is closed with end-kyc-session and the operator makes a decision.

Flow definition (flow)

Which documents to collect, whether a video call is required, and where the result notification should be sent are all kept in the flow definition. Flows are created from the admin panel and referred to by a code (e.g. onboarding). You send this code when creating a session.

A flow determines the following:

Create session

POST /api/document/create-session/ Bearer

Opens a KYC session, or returns the already-open session for the same customer and flow.

Fields

Field Type Description
customer_ref required text Customer reference
flow text Flow code, e.g. onboarding
device_id text The end user's device identifier
ip_address text The end user's IP address
user_agent text The end user's browser information
geo object Location data
Device and network information must come from you

This endpoint is called by your server, not by the end user. As a result, the request's own IP address and browser information belong to your server and say nothing about the end user. For risk scoring to work correctly, include the end user's ip_address, device_id, and user_agent in the request body.

Example request

POST /api/document/create-session/
Authorization: Bearer <token>
Content-Type: application/json

{
  "customer_ref": "C-1001",
  "flow": "onboarding",
  "device_id": "a91f3c7d20e4",
  "ip_address": "88.230.14.7",
  "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) …",
  "geo": { "lat": 41.0082, "lon": 28.9784 }
}

Example response | 200

{
  "status": true,
  "message": "Processed verification session record.",
  "data": {
    "session": {
      "session_id": "1aa3d05c-fde7-410c-b0e1-090487760c47",
      "flow": "onboarding",
      "required_artifact_types": ["doc_front", "doc_mrz", "selfie"],
      "pending_artifact_types":  ["doc_mrz", "selfie"],
      "succeeded_artifact_types": ["doc_front"],
      "requires_video_call": false,
      "attempt_no": 1,
      "livekit_token": "",
      "endpoint_token": "eyJjdXN0b21lcl9yZWYiOiJDLTEwMDEi…",
      "mrz_data": null
    }
  }
}
Field Description
session_id The session's unique identifier
endpoint_token The signed token the end user will use when uploading documents. Valid for 24 hours.
required_artifact_types The document types required by the flow
succeeded_artifact_types Types that have been uploaded and successfully verified
pending_artifact_types Types that are still missing; build your interface around this
requires_video_call Whether the flow requires a video call
attempt_no The attempt number. Increments when returning to an open session
livekit_token The video call token. Empty if the flow doesn't require a call
mrz_data The fields extracted if the MRZ was read, otherwise null
Repeating the same call is safe

If you call again with the same customer_ref and flow, no new session is opened; the already-open session is returned and attempt_no increments by one. A different flow opens a separate session. This lets you build flows that resume where the user left off.

Errors

Status Description
400 Unknown or inactive verification flow: <code>: the flow was not found or is inactive. No session is opened
401 Invalid Bearer token

Upload document

POST /api/document/upload_document/ session token multipart/form-data

Called from the end user's browser. Authentication is established with the endpoint_token you received from create-session.

Form fields

Field Type Description
token required text The endpoint_token received from create-session
artifact_type required text See document types
snapshot required file Up to 25 MB, image/* or video/*
extra_data JSON text Structured data accompanying the document (e.g. fields read from an NFC chip). Silently ignored if it isn't valid JSON

Example request

const formData = new FormData();
formData.append('token', endpointToken);
formData.append('artifact_type', 'doc_front');
formData.append('snapshot', file);

await fetch('/api/document/upload_document/', {
    method: 'POST',
    body: formData      // DO NOT manually add the Content-Type header
});

Response | 200

{
  "status": "success",
  "path": "snapshots/2026-08-07/1aa3d05c…__985545f0….jpg",
  "artifact_id": "744a654d-3350-4f13-8de7-2f88d888e739",
  "artifact_type": "doc_front"
}

Errors

Status reason Description
401 token_missing Token not sent
401 token_invalid Token is malformed or the signature is invalid
401 token_expired Token is older than 24 hours; create a new session
403 token_wrong_audience Agent tokens cannot be used on this endpoint
404 session_not_found The session referenced by the token does not exist
409 session_closed The session has been resolved; documents cannot be added to a decided case
400 artifact_type_missing / artifact_type_invalid Document type is missing or undefined
400 file_missing / file_empty File is missing or empty
413 file_too_large The 25 MB limit was exceeded
415 file_type_not_allowed Only images and videos are accepted

The error body uses the standard envelope:

{
  "status": false,
  "message": "This verification session is closed (decided).",
  "data": { "reason": "session_closed" }
}

Take snapshot

POST /api/document/take_snapshot/ session token multipart/form-data

Saves the frame captured from the end user's camera during the video call. Requires the same token as upload_document; the document type is automatically assigned as snapshot.

Field Description
token required endpoint_token
snapshot required Image file, 25 MB maximum
{
  "status": "success",
  "path": "snapshots/2026-08-07/1aa3d05c…__985545f0….jpg",
  "artifact_id": "744a654d-3350-4f13-8de7-2f88d888e739"
}

Error codes are the same as upload_document.

End session

POST /api/document/end-kyc-session/ agent token

Ends the call and writes the checklist marked by the agent to the audit record.

Agent token required

This endpoint cannot be called with a customer token; it returns 403. The token to use is a separate one generated when the agent joins the call. This way only an agent who actually joined the room can end the call.

Request

Field Description
token required Agent session token
status required completed, ended or reported
checklist Checklist answers, {"101": true, "102": false}
Submitted status Session status
completed Completed
reported Reported
ended Failed

Response | 200

{
  "status": true,
  "session_id": "1aa3d05c-fde7-410c-b0e1-090487760c47",
  "session_status": "completed"
}

An undefined status value is rejected with 400; it is never silently treated as "failed". Closing an already-closed session again is not an error (page reload, double click).

How it works

Document verification runs asynchronously. The upload_document call only tells you "file received"; operations such as OCR, MRZ reading and face matching run in the background and can take a few seconds.

You don't need to keep polling to wait for the result: as soon as each verification finishes, a websocket message is sent to the relevant session.

document uploaded  →  background verification  →  websocket message  →  your UI
Messages are session-specific

Every message is routed to a single verification session. When you register for a session you only receive that session's results; other customers' results never reach you.

Connection and registration

After opening the websocket connection, you need to send a registration message indicating which session you are listening to. The endpoint_token you received from create-session is used as identity, the same token as the document upload endpoints.

const ws = new WebSocket("wss://<websocket-host>/document/");

ws.onopen = () => {
    ws.send(JSON.stringify({
        event: "register",
        token: endpointToken        // endpoint_token from create-session
    }));
};

ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    if (msg.event !== "broadcast") return;

    const { type, result } = msg.data;
    if (result.code === "success") {
        // mark the step as completed
    } else {
        // ask the user to retry: result.message
    }
};
The token's expiry covers the connection too

endpoint_token is valid for 24 hours. You cannot register with an expired token; you need to open a new session.

Result messages

Message envelope

Every incoming message has the same structure:

{
  "event": "broadcast",
  "client_id": "...",
  "timestamp": 1786111190365,        // epoch, milliseconds
  "correlation_id": "9f3c1a20-…",    // to track a single notification
  "data": {
    "to": "1aa3d05c-fde7-410c-b0e1-090487760c47",   // session id
    "delivery": "transporter",
    "type": "mrz_verification_result",              // which verification
    "result": {
      "code": "success",                            // success | fail
      "data": { … },                                // extra data, null if none
      "message": "MRZ text has been successfully read."
    }
  }
}
Three fields are the same in every message

result.code is only ever success or fail; no other value is sent. result.data is present in every message; if there is nothing to carry it is null, the field is never omitted. result.message is a description that can be shown to the user. Because these three are fixed, a single handler is enough.

Message types

type When it arrives result.data
docfront_verification_result When the ID document front (doc_front) is processed Check details (was OCR read, was a face found, mismatched fields)
mrz_verification_result When the MRZ image (doc_mrz) is processed Fields read from the MRZ (first name, last name, document number, date of birth and expiry …)
nfc_raw_result When the NFC chip data (nfc_raw) is processed null
face_verification_result When the selfie (selfie) is compared against the photo on the document null
liveness_result When the liveness video (liveness_video) is processed null
recording_result When the call recording (recording) is processed null
liveness_result is not yet proof of anything

Liveness detection does not work at the moment: the video is captured and stored, but since its content is not analyzed this message always returns success. Do not base your liveness decision on it. When detection is enabled the message format will not change, only code will start reflecting the actual result.

Example: successful MRZ read

{
  "event": "broadcast",
  "timestamp": 1786111190365,
  "correlation_id": "9f3c1a20-4b77-4e51-8a03-2d5e91c4f8a1",
  "data": {
    "to": "1aa3d05c-fde7-410c-b0e1-090487760c47",
    "delivery": "transporter",
    "type": "mrz_verification_result",
    "result": {
      "code": "success",
      "data": {
        "first_name": "AYSE",
        "last_name": "YILMAZ",
        "document_number": "A12B34567",
        "birth_date": "1990-04-17",
        "expiry_date": "2031-06-30",
        "nationality": "TUR"
      },
      "message": "MRZ text has been successfully read."
    }
  }
}

Example: failed face match

{
  "event": "broadcast",
  "timestamp": 1786111203881,
  "correlation_id": "c07be413-1f9a-42d0-9c62-b8ad5e3a7710",
  "data": {
    "to": "1aa3d05c-fde7-410c-b0e1-090487760c47",
    "delivery": "transporter",
    "type": "face_verification_result",
    "result": {
      "code": "fail",
      "data": null,
      "message": "Yüzler eşleşmedi, lütfen tekrar deneyin."
    }
  }
}

Handling recommendations

Constants

Document types (artifact_type)

Value Description
doc_front Front of ID document
doc_back Back of ID document
doc_mrz MRZ zone only (faster)
nfc_raw NFC chip data and biometric photo
passport_mrz_page Passport MRZ page
selfie Selfie
liveness_video Liveness video
snapshot Frame captured during the call
recording Call recording

Customer type (customer_type)

Value Meaning
1 Individual (default)
2 Corporate

Gender (gender)

Value Meaning
male Male
female Female
other Other
unknown Unknown

Transaction direction (io_type)

Value Meaning
0 Credit (default)
1 Debit

Channel (channel)

Value Meaning
0 Unknown (default)
1 Web
2 Mobile browser
3 Mobile / Android
4 Mobile / iOS

Decision (decision)

Value Meaning
ACCEPT Accepted
REVIEW Under review
REJECT Rejected

Event type (event_type)

Selected when defining an event in the admin panel; determines which endpoint it is sent to.

Value Meaning Endpoint
0 Onboarding event non-financial
1 Financial event financial
2 Non-financial event non-financial

Error codes

KYC endpoints include a data.reason field in their error bodies. These codes are stable and can be used safely in code; the message text may change.

reason Status What to do
token_missing 401 Add the token to the request
token_invalid 401 Token is corrupted; start a new session
token_expired 401 24 hours have passed; start a new session
token_wrong_audience 403 Wrong token type used
session_not_found 404 Session deleted or ID incorrect
session_closed 409 Session already finalized; start a new session
artifact_type_missing 400 Send the document type
artifact_type_invalid 400 Use one of the defined types
file_missing 400 File field is empty
file_empty 400 File is 0 bytes
file_too_large 413 Compress the image (limit 25 MB)
file_type_not_allowed 415 Only send images and videos
status_invalid 400 Use one of the defined closing statuses