# API Reference
# Introduction
Sum&Substance's API allows you to send and receive applicants’ data and documents for verification through simple RESTful APIs. API responds with JSON payload (unless otherwise stated). When data payload should be provided, it expects parameters to be passed in valid JSON (except when stated otherwise) with the Content-Type: application/json
header being set.
All incompatible changes made to API will be versioned and won't affect existing endpoints. However, we may add additional fields to JSON response without any notice or API versioning. So it's not a good idea to rely on object mappers in your integration code that will fail on unknown properties.
Note that header names are not case-sensitive and may vary depending on your ways of implementation at both your requests and our responses.
We have two environments:
- Sandbox: this is only needed when testing out your integration with Sumsub. No real checks are performed when in the sandbox environment.
- Production: In order to provide you with access to the production environment, we require that the integration is tested first. We just want to make sure that everything works the way it should.
Both environments are available via api.sumsub.com
and are controlled by the switch in the top right-hand corner of the dashboard.
API accesses are regulated through the use of the X-App-Token
authorization header value.
# Postman
You can download, import and run the Sumsub API Postman collection:
Download ⤓In your Postman environment, you'll need to define the variables app-token
and secret-key
.
You can read more in Postman’s documentation about managing environments..
# Authentication
All API queries must be made over HTTPS, and plain HTTP will be refused. You must include your X-App headers in all requests.
# App Tokens
This is the most secure method of talking to our API programmatically.
You can generate App Tokens in the dev space on our dashboard.
Full sized App Token and Secret Key values are shown only once at the dashboard in the moment of App Token creation, and their settings cannot be changed later.
# Making a request
All requests must contain the following headers:
X-App-Token
- an App Token that you generate in our dashboardX-App-Access-Sig
- signature of the request in the hex format and lowercase (see below)X-App-Access-Ts
- number of seconds since Unix Epoch in UTC
# Signing a request
The value of the X-App-Access-Sig
is generated by a sha256 HMAC algorithm using a secret key (provided upon App Token generation) on
the bytes obtained by concatenating the following information:
- A timestamp (value of the
X-App-Access-Ts
header) taken as a string - An HTTP method name in upper-case, e.g.
GET
orPOST
- URI of the request without a host name, starting with a slash and including all query parameters, e.g.
/resources/applicants/123?fields=info
- Request body, taken exactly as it will be sent. If there is no request body, e.g., for
GET
requests, don't include it.
Example of the string to be signed to get an accessToken: 1607551635POST/resources/accessTokens?userId=cfd20712-24a2-4c7d-9ab0-146f3c142335&levelName=basic-kyc-level&ttlInSecs=600
Your timestamp must be within 1 minute of the API server time. Make sure the time on your servers is correct.
# Examples
Here are some examples how you can sign requests
Language | App Token usage example |
---|---|
JS | Example |
Java | Example |
PHP [Guzzle] | Example |
Python | Example |
Ruby | Example |
GO | Example |
C# | Example |
# Access tokens for SDKs
To initialize SDK for Sandbox environment (to not create a real verification case on production) make sure to use App token and Secret key pair that was created on Sandbox for request authorization headers.
When initializing WebSDK or MobileSDK, an access token authentication must be used.
POST /resources/accessTokens?userId={userId}&levelName={levelName}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
ttlInSecs | Integer | No | Lifespan of a token in seconds. Default value is equal to 10 minutes. |
userId | String | Yes | An external user ID which will be bound to the token. It correlates to externalUserId of an applicant. |
levelName | String | Yes | A name of the level configured in the dashboard. |
externalActionId | String | No | An external action ID which will be bound to the token. More info about applicant actions you can find here. |
If your userId
or levelName
contains reserved characters (e.g., "@", "+", white spaces as %20
) it should be URL encoded otherwise you may get signature mismatch or just an invalid parameter value.
# RESPONSE
Name | Type | Description |
---|---|---|
token | String | A newly generated access token for an applicant. |
An access token for applicant has limited access to the API, e.g., it’s only valid for 1 applicant and can't access other applicants.
# Creating an access token for applicant
curl -X POST \
'https://api.sumsub.com/resources/accessTokens?userId=JamesBond007&levelName=basic-kyc-level&ttlInSecs=600' \
-H 'Accept: application/json'
# Example response
{
"token": "eyJhbGciOiJub25lIn0.eyJqdGkiOiJfYWN0LTZmODI2ZTU0LTE2MzctNDViMS05NzMyLWY1MjZiN2YxNWE3YyIsInVybCI6Imh0dHBzOi8vYXBpLnN1bXN1Yi5jb20ifQ.",
"userId": "JamesBond007"
}
Make sure your integration code does not validate or analyze the access token content, as the format is not fixed and may undergo further changes in the future. The token must be treated as an arbitrary string with the maximum length of 1KB.
You can also use a request body to provide us with parameters to create an accessToken.
POST /resources/accessTokens
# REQUEST BODY
Name | Type | Required | Description |
---|---|---|---|
ttlInSecs | Integer | No | Lifespan of a token in seconds. Default value is equal to 10 minutes. |
userId | String | Yes | An external user ID which will be bound to the token. It correlates to externalUserId of an applicant. |
levelName | String | Yes | A name of the level configured in the dashboard. |
externalActionId | String | No | An external action ID which will be bound to the token. More info about applicant actions you can find here. |
# Example request
curl -X POST \
'https://api.sumsub.com/resources/accessTokens' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"userId": "someUniqueUserId",
"levelName": "basic-kyc-level",
"ttlInSecs": 1800
}'
# Example response
{
"token": "_act-b8ebfb63-5f24-4b89-9c08-5bbabeec986e",
"userId": "JamesBond007"
}
# API Health
You can check operational status of our service using request below.
HTTP status 200
in response confirms availability of our API.
Note that the request should be authorized with headers mentioned above.
GET /resources/status/api
# Example request
curl -X GET \
'https://api.sumsub.com/resources/status/api'
You may also check our uptime history and current service status at the Sumsub Status page.
# Errors
On API errors we return standard HTTP Status Codes.
Please note that we have a limit for the following API requests that you may send. If there are too many requests from you, we will return status code 429
.
Please notify our team if you have to make a vast amount of requests during a particular time frame.
Response body contains a JSON payload with additional information. For example:
{
"description": "Error analyzing file, unsupported format or corrupted file",
"code": 400,
"correlationId": "req-5fd59b09-7f5e-41cd-a86b-38a4e6d57e08",
"errorCode": 1004, // may be absent
"errorName": "corrupted-file" // may be absent
}
# Response body
Name | Type | Optional | Description |
---|---|---|---|
description | String | No | Human-readable error description. |
code | Integer | No | HTTP Status code. |
correlationId | String | No | This id uniquely identifies the error. You can send this id to us, in case the cause of the problem is still unclear. |
errorCode | Integer | Yes | Code of the exact problem (see the Error codes section). For some errors may not be present. |
errorName | String | Yes | String representation of the exact problem (see the Error codes section). It always appears when errorCode presents. |
# Error codes
Please, note that not all error responses will contain error code and name. More and more errors will get their codes in the future.
Code | Name | Description |
---|---|---|
1000 | duplicate-document | Duplicate document (image, video) was uploaded. Exact equality is taken into account. |
1001 | too-many-documents | Applicant contains too many documents. Adding new ones is not allowed. |
1002 | file-too-big | Uploaded file is too big (more than 64MB). |
1003 | empty-file | Uploaded file is empty (0 bytes). |
1004 | corrupted-file | File is corrupted or incorrect format (e.g. PDF file is uploaded as JPEG). |
1005 | unsupported-file-format | Unsupported file format (e.g. a TIFF image). |
1006 | no-upload-verification-in-progress | Applicant is being checked. Adding new data is not allowed. |
1007 | incorrect-file-size | The file size should be more or less than custom values from global settings at the dashboard. |
1008 | applicant-marked-as-deleted | Applicant is marked as deleted/inactive. No action allowed to change its status. |
1009 | applicant-with-final-reject | Applicant is rejected with rejection type of FINAL . No adding new data/files allowed. |
1010 | doc-type-not-in-req-docs | Attempt to upload a document outside of the applicant level set/set of required documents. |
3000 | applicant-is-already-in-the-state | An attempt to change the status of the applicant against the logic - applicant is already in required state. |
4000 | app-token-invalid-format | Invalid format of X-App-Token value. |
4001 | app-token-not-found | App token doesn't exist (e.g. test env. token used on production). |
4002 | app-token-private-part-mismatch | Private part of the token (after dot) doesn't match public part. |
4003 | app-token-signature mismatch | Signature encoded value doesn't match request content. |
4004 | app-token-request-expired | X-App-Access-Ts doesn't match to the number of seconds since Unix Epoch in UTC. |
4005 | app-token-invalid-value | Invalid authentication header values were provided. |
4006 | app-token-not-all-auth-params-provided | Not all required authorization headers were provided. |
4007 | app-token-invalid-params | Invalid authentication parameters were provided. |
5000 | applicant-already-blacklisted | Attempt to blocklist an applicant that is already blocklisted. |
5001 | applicant-already-whitelisted | Attempt to whitelist an applicant that is already whitelisted. |
If you still don't understand what the issue is, please, contact us with the correlationId
provided.
# Applicants API
# Creating an applicant
An applicant is an entity representing one physical person. It may have several ID documents attached, like an ID card or a passport. Many additional photos of different documents can be attached to the same applicant.
POST /resources/applicants?levelName={levelName}
When creating an applicant, you don't have to send us prefilled applicant data such as names, dob, addresses, etc. You only need to send us images and get the recognized document data by this method. It's the best way to increase your conversion because rejects of mismatches in names and typos will be excluded.
If you have requirements to make data cross-validation and have no possibility to perform it on your side you can use fixedInfo
object to fill data like names, date of birth and address.
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
levelName | String | Yes | Name of user verification steps configuration that you should set up at the dashboard. |
#{body} | Object | Yes | An object representing an applicant (see example). |
Please note that the levelName
is case-sensitive and has to be created at the same environment.
# REQUEST BODY
Name | Type | Required | Description |
---|---|---|---|
externalUserId | String | Yes | An applicant ID on the client side, should be unique. |
sourceKey | String | No | If you want to separate your clients that send applicants, provide this field to distinguish between them. It also shows up at the webhook payloads. |
String | No | Applicant email. | |
phone | String | No | Applicant phone number. |
lang | String | No | The language in which the applicant should see the result of verification in ISO 639-1 format. |
metadata | Array of objects | No | Additional information that is not displayed to the end user ( Example: [{"key": "keyFromClient", "value": "valueFromClient"}] ). |
fixedInfo | Object | No | Basic information about the applicant that we shouldn't change from our side but cross-validate it with data recognized from documents. Has the same attributes as info . |
# fixedInfo
and info
ATTRIBUTES
Name | Type | Required | Description |
---|---|---|---|
firstName | String | No | First name. |
lastName | String | No | Last name. |
middleName | String | No | Middle name. |
firstNameEn | String | No | Automatic transliteration of the first name. |
lastNameEn | String | No | Automatic transliteration of the last name. |
middleNameEn | String | No | Automatic transliteration of the middle name. |
legalName | String | No | Legal name. |
gender | String | No | Sex of a person (M or F ). |
dob | String | No | Date of birth (format YYYY-mm-dd , e.g. 2001-09-25). |
placeOfBirth | String | No | Place of birth. |
countryOfBirth | String | No | Country of birth. |
stateOfBirth | String | No | State of birth. |
country | String | No | Alpha-3 country code (e.g. DEU or GBR ) (Wikipedia). |
nationality | String | No | Alpha-3 country code (Wikipedia). |
addresses | Array | No | List of addresses. |
tin | String | No | Tax Identification Number. |
# addresses
ELEMENTS FIELDS
Name | Type | Required | Description |
---|---|---|---|
country | String | No | Alpha-3 country code. |
postCode | String | No | Postal code. |
town | String | No | Town or city name. |
street | String | No | Street name. |
subStreet | String | No | Additional street information. |
state | String | No | State name if applicable. |
buildingName | String | No | Building name if applicable. |
flatNumber | String | No | Flat or apartment number. |
buildingNumber | String | No | Building number. |
An applicant entity with the associated ID (refer to the example). Persist the id
field after creating the applicant -
it will be needed for further requests that you intend to make.
For the IDENTITY
step we recommend the following document types: PASSPORT
, ID_CARD
, RESIDENCE_PERMIT
, DRIVERS
If levelName
contains reserved characters (e.g., "@", "+", white spaces as %20
) it should be URL encoded otherwise you may get signature mismatch.
The same applies to all endpoints below.
# Example request
curl -X POST \
'https://api.sumsub.com/resources/applicants?levelName=basic-kyc-level' \
-H 'Content-Type: application/json' \
-d '{
"externalUserId": "someUniqueUserId",
"email": "[email protected]",
"phone": "+449112081223",
"fixedInfo": {
"country": "GBR",
"placeOfBirth": "London"
}
}'
# Example response
{
"id": "5f80e6b7155a6336271e4677", //applicantId
"createdAt": "2020-10-09 22:39:51",
"clientId": "SumsubClient",
"inspectionId": "5f80e6b7155a6336271e4678",
"externalUserId": "someUniqueUserId",
"fixedInfo": {
"placeOfBirth": "London",
"country": "GBR"
},
"email": "[email protected]",
"phone": "+449112081223",
"requiredIdDocs": {
"excludedCountries": [
"PRK"
],
"docSets": [
{
"idDocSetType": "IDENTITY",
"types": [
"PASSPORT",
"DRIVERS",
"ID_CARD",
"RESIDENCE_PERMIT"
]
},
{
"idDocSetType": "SELFIE",
"types": [
"SELFIE"
],
"videoRequired": "disabled"
}
]
},
"review": {
"reprocessing": false,
"createDate": "2020-10-09 22:39:51+0000",
"reviewStatus": "init"
},
"type": "individual"
}
# Changing required document set (level)
To update the list of required documents based on the level provided, you can use the following method. For example, if you initially required only an ID document and selfie for verification, and now need to add a proof of address, you can simply add this additional step to the list of required documents.
If you're using our SDKs you can just initialize it with suitable levelName
on accessToken and SDK will change level automatically.
POST /resources/applicants/{applicantId}/moveToLevel?name={levelName}
Make sure to check the applicant status before changing the level. You won't be able to change level if reviewStatus
of the applicant is pending
, queued
or onHold
.
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
levelName | String | Yes | Name of new user verification steps configuration from the dashboard. |
# RESPONSE
A new requiredIdDocs
and levelName
will be returned.
# Example request with body for changing required document set
curl -X POST \
'https://api.sumsub.com/resources/applicants/5f80e6b7155a6336271e4677/moveToLevel?name=kyc-with-poa' \
-H 'Content-Type: application/json' \
# Example response
{
"id": "5f80e6b7155a6336271e4677",
"createdAt": "2020-10-09 22:47:58",
"clientId": "SumsubClient",
"inspectionId": "5f80e6b7155a6336271e4678",
"externalUserId": "someUniqueUserId",
"info": {
"firstName": "John",
"firstNameEn": "John",
"lastName": "Smith",
"lastNameEn": "Smith",
"dob": "2000-03-04",
"placeOfBirth": "London",
"placeOfBirthEn": "London",
"country": "GBR",
"phone": "+449112081223"
},
"email": "[email protected]",
"requiredIdDocs": {
"docSets": [
{
"idDocSetType": "IDENTITY",
"types": [
"PASSPORT",
"DRIVERS",
"ID_CARD",
"RESIDENCE_PERMIT"
]
},
{
"idDocSetType": "SELFIE",
"types": [
"SELFIE"
],
"videoRequired": "disabled"
},
{
"idDocSetType": "PROOF_OF_RESIDENCE",
"types": [
"UTILITY_BILL"
]
}
]
},
"review": {
"reprocessing": false,
"levelName": "kyc-with-poa",
"createDate": "2020-10-09 22:47:58+0000",
"reviewStatus": "init"
},
"type": "individual"
}
If you'd like to get current requiredIdDocs
you may call POST /resources/applicants/{applicantId}/requiredIdDocs
# Example request body for getting current required document set
curl -X POST \
'https://api.sumsub.com/resources/applicants/5f80e6b7155a6336271e4677/requiredIdDocs' \
-H 'Content-Type: application/json'
# Adding an ID document
This method gets a multipart form: document JSON metadata and, optionally, a document photo. If a document with the same metadata type and country already exists, the new data will be merged with the existing data. Any existing data will be overwritten if it is also present in the new object. However, a new image will always be added. If you don't yet know the document metadata, please include the type and country fields, which are mandatory. For example, you might send "PASSPORT" and "GBR".
POST /resources/applicants/{applicantId}/info/idDoc
# REQUEST HEADERS
Name | Type | Value |
---|---|---|
Content-Type | String | multipart/form-data |
X-Return-Doc-Warnings | Boolean | true /false |
For adding a Proof of Address or Identity type of documents it's advised to use header X-Return-Doc-Warnings
with value of true
which allows determining if document data is readable and acceptable before moving the applicant to pending status:
in response errors
or warnings
array will present. If you happen to encounter an error
during the upload process, please note that the image you uploaded will be marked as inactive. This means that it won't be used as the primary document for the verification step. You will need to upload another image in order to proceed with the verification process successfully.
For double-sided documents (FRONT_SIDE
and BACK_SIDE
of one idDocType value) both images will be marked inactive even if you've got an error for only one of them.
Array of warnings
just makes a notice that there might be something wrong with a document.
Warnings and errors are shown only for the first four attempts to upload a document. For next cases we assume that document should be checked within the full verification process.
We may change country
and idDocType
values that were sent to us on image upload, so if that sort of mapping is important to your side, make sure to compare these object from our response.
Please note that it's a simple and fast check and may not send errors for some document types to provide better conversion.
Note that applicant won't able to be moved to pending
status for a check if one of verification steps is not active (doesn't have active images within itself). To check if a verification step is active you can call this method.
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
# FORM DATA
Name | Type | Required | Description |
---|---|---|---|
metadata | Object | Yes | An object representing an ID document |
content | Binary | No | A photo of a document |
# REQUEST metadata
BODY PART FIELDS
Name | Type | Required | Description |
---|---|---|---|
idDocType | String | Yes | See supported document types here |
idDocSubType | String | No | FRONT_SIDE , BACK_SIDE or null |
country | String | Yes | 3-letter country code (Wikipedia) |
firstName | String | No | First name |
middleName | String | No | Middle name |
lastName | String | No | Last name |
issuedDate | String | No | Issued date (format YYYY-mm-dd , e.g. 2001-09-25) |
validUntil | String | No | Valid until date (format YYYY-mm-dd , e.g. 2001-09-26) |
number | String | No | Document number |
dob | String | No | Date of birth |
placeOfBirth | String | No | Place of birth |
- If a document is double-sided submit two images and set up
idDocSubType
properly (FRONT_SIDE
andBACK_SIDE
) - Make sure to send
BACK_SIDE
ifFRONT_SIDE
was already sent otherwise verification step won't be completed, and you won't be able to initiate a check.
# RESPONSE
JSON representing added document information.
If you need to know the imageId
of the photo, you can find this information in the response header X-Image-Id
# Available values of errors
array
Name | Description |
---|---|
forbiddenDocument | Unsupported or unacceptable type/country of document. |
differentDocTypeOrCountry | Document type or country mismatches ones that was sent with metadata and the recognized type is forbidden by settings. |
missingImportantInfo | Not all required document data can be recognized. |
dataNotReadable | There is no available data to recognize from image. |
expiredDoc | Document validity date is expired. |
documentWayTooMuchOutside | Some parts of the document are cropped. |
noIdDocFacePhoto | Face is not clearly visible on the document. |
selfieFaceBadQuality | Face is not clearly visible on the selfie. |
screenRecapture | Image might be a photo of screen. |
screenshot | Image is a screenshot. |
sameSides | Image of the same side of document was uploaded as front and back sides. |
shouldBeMrzDocument | Sent document type should have an MRZ, but there is no readable MRZ on the image. |
shouldBeDoubleSided | Two sides of the sent document should be presented. |
shouldBeDoublePaged | The full double-page of the document (usually, two main passport pages) are required. |
documentDeclinedBefore | The same image was uploaded and declined earlier in the same applicant. |
# Available values of warnings
array
Name | Description |
---|---|
badSelfie | Make sure that your face and the photo in the document are clearly visible. |
dataReadability | Please make sure that the information in the document is easy to read. |
inconsistentDocument | Please ensure that all uploaded photos are of the same document. |
maybeExpiredDoc | Your document appears to be expired. |
documentTooMuchOutside | Please ensure that the document completely fits the photo. |
# Example request
curl -X POST \
'https://api.sumsub.com/resources/applicants/5b75a5e80a975a3ef2102a87/info/idDoc' \
-H 'Content-Type: multipart/form-data' \
-H 'X-Return-Doc-Warnings: true' \
-F 'metadata={"idDocType":"PASSPORT","country":"USA"}' \
-F 'content=@/SumSub/Example/name.jpg'
# Example response
{
"idDocType": "PASSPORT",
"country": "USA",
"errors": [ "dataNotReadable" ] // will be absent in case of X-Return-Doc-Warnings not having value of "true"
}
In cases when only the data of the document needs to upload:
curl -X POST \
'https://api.sumsub.com/resources/applicants/5b75a5e80a975a3ef2102a87/info/idDoc' \
-H 'Content-Type: multipart/form-data' \
-F 'metadata={
"idDocType": "PASSPORT",
"country": "GBR",
"number": "123456789",
"issuedDate": "2015-01-02",
"dob": "2000-02-01",
"placeOfBirth": "London"
}'
# Example response
{
"idDocType": "PASSPORT",
"country": "GBR",
"issuedDate": "2015-01-02",
"number": "40111234567",
"dob": "2000-02-01",
"placeOfBirth": "London"
}
# Supported document types
Value | Description |
---|---|
ID_CARD | An ID card |
PASSPORT | A passport |
DRIVERS | A driving license |
RESIDENCE_PERMIT | Residence permit or registration document in the foreign city/country |
UTILITY_BILL | Proof of address document. Check here for the full list of acceptable docs as UTILITY_BILL |
SELFIE | A selfie with a document |
VIDEO_SELFIE | A selfie video (can be used in webSDK or mobileSDK) |
PROFILE_IMAGE | A profile image, i.e. avatar (in this case no additional metadata should be sent) |
ID_DOC_PHOTO | Photo from an ID doc (like a photo from a passport) (No additional metadata should be sent) |
AGREEMENT | Agreement of some sort, e.g. for processing personal info |
CONTRACT | Some sort of contract |
DRIVERS_TRANSLATION | Translation of the driving license required in the target country |
INVESTOR_DOC | A document from an investor, e.g. documents which disclose assets of the investor |
VEHICLE_REGISTRATION_CERTIFICATE | Certificate of vehicle registration |
INCOME_SOURCE | A proof of income |
PAYMENT_METHOD | Entity confirming payment (like bank card, crypto wallet, etc) |
BANK_CARD | A bank card, like Visa or Maestro |
COVID_VACCINATION_FORM | COVID vaccination document (may contain the QR code) |
OTHER | Should be used only when nothing else applies |
# Getting applicant data
During the verification, we also extract data from the applicant's ID docs. To get the full structured view of an applicant, you should perform the following request.
GET /resources/applicants/{applicantId}/one
or
GET /resources/applicants/-;externalUserId={externalUserId}/one
You may want to use the second request when you don't know an applicant ID yet. E.g., when WebSDK created an applicant for you and we called your webhook endpoint.
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant id |
externalUserId | String | Yes | User id in your system |
# RESPONSE
Response Code | Response | Reason |
---|---|---|
200 | Applicant entity | |
400 | Error JSON | Invalid applicant id provided |
404 | Error JSON | Applicant not found |
The existence of each field depends on the documents submitted for verification and the regulations of verification.
It's basically an applicant that you've created (or we've created for you, e.g., via WebSDK)
with augmented information. The info.idDocs[]
array of objects contains information extracted from applicants’ documents which can be useful in cases where a verification has been completed.
To make sure that you've fetched data from a relevant document you can get what idDocType
and country
was approved using this method.
Note that particular data is shown only if it was recognized from the document or provided by user.
# RESPONSE ATTRIBUTES
Name | Type | Optional | Description |
---|---|---|---|
id | String | No | An applicantId. |
inspectionId | String | No | Inspection ID. |
externalUserId | String | No | An applicant ID on the client side, should be unique. |
sourceKey | String | Yes | If you want to separate your clients that send applicants, provide this field to distinguish between them. It also shows up at the webhook payloads. |
String | Yes | Applicant email. | |
phone | String | Yes | Applicant phone number. |
lang | String | Yes | The language in which the applicant should see the result of verification in ISO 639-1 format. |
metadata | Array of objects | Yes | Additional information that is not displayed to the end user ( Example: [{"key": "keyFromClient", "value": "valueFromClient"}] ). |
fixedInfo | Object | Yes | Basic information about the applicant that we shouldn't change from our side but cross-validate it with data recognized from documents. Has the same attributes as info . |
createdAt | Date | No | Time and date of applicant creation. |
requiredIdDocs | Object | Yes | Object that describes the set of required documents and data for applicant to upload and pass verification. |
review | Object | No | Object that describes current applicant status. |
questionnaires | Array of objects | Yes | Contains data about filled questionnaire. More info is here. |
# fixedInfo
and info
ATTRIBUTES
Name | Type | Optional | Description |
---|---|---|---|
firstName | String | Yes | First name. |
lastName | String | Yes | Last name. |
middleName | String | Yes | Middle name. |
firstNameEn | String | Yes | Automatic transliteration of the first name. |
lastNameEn | String | Yes | Automatic transliteration of the last name. |
middleNameEn | String | Yes | Automatic transliteration of the middle name. |
legalName | String | Yes | Legal name. |
gender | String | Yes | Sex of a person (M or F ). |
dob | String | Yes | Date of birth (format YYYY-mm-dd , e.g. 2001-09-25). |
placeOfBirth | String | Yes | Place of birth. |
country | String | Yes | Alpha-3 country code (e.g. DEU or GBR ) (Wikipedia). |
nationality | String | Yes | Alpha-3 country code (Wikipedia). |
addresses | Array of objects | Yes | List of addresses. |
idDocs | Array of objects | Yes | Represents the set of data recognized from uploaded documents. |
tin | String | Yes | Tax Identification Number. |
# addresses
ELEMENTS FIELDS
Name | Type | Optional | Description |
---|---|---|---|
country | String | Yes | Alpha-3 country code. |
postCode | String | Yes | Postal code. |
town | String | Yes | Town or city name. |
street | String | Yes | Street name. |
subStreet | String | Yes | Additional street information. |
state | String | Yes | State name if applicable. |
# Example request
curl -X GET \
'https://api.sumsub.com/resources/applicants/5b594ade0a975a36c9349e66/one'
# Example response
{
"id": "5b594ade0a975a36c9349e66",
"createdAt": "2020-06-24 05:05:14",
"clientId": "ClientName",
"inspectionId": "5b594ade0a975a36c9379e67",
"externalUserId": "SomeExternalUserId",
"fixedInfo": {
"firstName": "Chris",
"lastName": "Smith"
},
"info": {
"firstName": "CHRISTIAN",
"firstNameEn": "CHRISTIAN",
"lastName": "SMITH",
"lastNameEn": "SMITH",
"dob": "1989-07-16",
"country": "DEU",
"idDocs": [
{
"idDocType": "ID_CARD",
"country": "DEU",
"firstName": "CHRISTIAN",
"firstNameEn": "CHRISTIAN",
"lastName": "SMITH",
"lastNameEn": "SMITH",
"validUntil": "2028-09-04",
"number": "LGXX359T8",
"dob": "1989-07-16",
"mrzLine1": "IDD<<LGXX359T88<<<<<<<<<<<<<<<",
"mrzLine2": "8907167<2809045D<<<<<<<<<<<<<8",
"mrzLine3": "SMITH<<CHRISTIAN<<<<<<<<<<<<<<"
}
]
},
"agreement": { //present when SDK was initialized with Agreement screen enabled
"createdAt": "2020-06-24 04:18:40",
"source": "WebSDK",
"targets": [
"By clicking Next, I accept [the Terms and Conditions](https://www.sumsub.com/consent-to-personal-data-processing/)",
"I agree to the processing of my personal data, as described in [the Consent to Personal Data Processing](https://sumsub.com/consent-to-personal-data-processing/)"
]
},
"email": "[email protected]",
"applicantPlatform": "Android",
"requiredIdDocs": {
"docSets": [
{
"idDocSetType": "IDENTITY",
"types": [
"PASSPORT",
"ID_CARD"
]
},
{
"idDocSetType": "SELFIE",
"types": [
"SELFIE"
]
}
]
},
"review": {
"elapsedSincePendingMs": 115879,
"elapsedSinceQueuedMs": 95785,
"reprocessing": true,
"levelName": "basic-kyc",
"createDate": "2020-06-24 05:11:02+0000",
"reviewDate": "2020-06-24 05:12:58+0000",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed"
},
"lang": "de",
"type": "individual"
}
Here, you can find examples of recognition results for all supported document types.
# Changing provided info (fixedInfo)
If you'd like to alter data that you've provided us to cross-check it with documents, you can issue a PATCH
request instead of creating a new applicant, which is highly discouraged. This method patches the fields in the fixedInfo
key of the applicant.
PATCH /resources/applicants/{applicantId}/fixedInfo
# REQUEST ARGUMENTS
The body of fixedInfo
must contain all those fields that you'd like to change.
Name | Type | Required | Description |
---|---|---|---|
#{body} | Object | Yes | Field in the fixedInfo attribute that should be changed |
applicantId | String | Yes | Applicant ID |
You can take a look at available fixedInfo
attributes here.
# Example request
curl -X PATCH \
'https://api.sumsub.com/resources/applicants/609d68bf460647000aa2d87e/fixedInfo' \
-H 'Content-Type: application/json' \
-d '{
"firstName" : "Bradley",
"lastName" : "Peak",
"dob" : "1990-01-01"
}'
# Getting verification results
When we are talking about verification results, we typically mean two labels:
Value | Description |
---|---|
GREEN | Everything is fine |
RED | Some violations found |
Once verification is finished, we will send you a POST request with JSON payload to the URL provided to us while integrating. Often times, there are different URLs for staging and production environments. We may send several types of webhooks, but the one of interest here is the applicantReviewed
webhook. This type of webhook is expected to arrive at average after 3-5 minutes, but can take up to 24 hours, in theory.
If for some reason you missed the webhooks, don't worry: we make a note of everything that was attempted to be sent, and can resend failed webhooks at any point in time. If webhook request fails, we attempt to resend it four times: after 5 minutes, 1 hour, 5 and 18 hours until request succeeds.
Check createdAtMs
field of webhook payload to make sure that you're getting relevant applicant status.
We recommend that you wait for the webhook no more than a day, and then send a request to our server for information about the status of the applicant.
Also you can monitor webhook statuses and resend them manually on the API Health
page of the dashboard.
Please note that the rejectLabels are only needed to analyze your statistics. They are generalized, so you should not use them to generate comments for your end user.
Webhook contains a reviewResult
field that contains extra information, please see example. An important field is reviewAnswer
, which contains a final highly trusted answer. Additionally, in cases of the RED
answer, the rejectLabels
field contains one or more of the following machine-readable constants:
Value | Default reviewRejectType | Description |
---|---|---|
FORGERY | FINAL | Forgery attempt has been made |
DOCUMENT_TEMPLATE | FINAL | Documents supplied are templates, downloaded from internet |
LOW_QUALITY | RETRY | Documents have low-quality that does not allow definitive conclusions to be made |
SPAM | FINAL | An applicant has been created by mistake or is just a spam user (irrelevant images were supplied) |
NOT_DOCUMENT | RETRY | Documents supplied are not relevant for the verification procedure |
SELFIE_MISMATCH | FINAL | A user photo (profile image) does not match a photo on the provided documents |
ID_INVALID | RETRY | A document that identifies a person (like a passport or an ID card) is not valid |
FOREIGNER | FINAL | When a client does not accept applicants from a different country or e.g. without a residence permit |
DUPLICATE | FINAL | This applicant was already created for this client, and duplicates are not allowed by the regulations |
BAD_AVATAR | RETRY | When avatar does not meet the client's requirements |
WRONG_USER_REGION | FINAL | When applicants from certain regions/countries are not allowed to be registered |
INCOMPLETE_DOCUMENT | RETRY | Some information is missing from the document, or it's partially visible |
BLACKLIST | FINAL | User is blocklisted by our side |
BLOCKLIST | FINAL | User is blocklisted by your side |
UNSATISFACTORY_PHOTOS | RETRY | There were problems with the photos, like poor quality or masked information |
DOCUMENT_PAGE_MISSING | RETRY | Some pages of a document are missing (if applicable) |
DOCUMENT_DAMAGED | RETRY | Document is damaged |
REGULATIONS_VIOLATIONS | FINAL | Regulations violations |
INCONSISTENT_PROFILE | FINAL | Data or documents of different persons were uploaded to one applicant |
PROBLEMATIC_APPLICANT_DATA | RETRY | Applicant data does not match the data in the documents |
ADDITIONAL_DOCUMENT_REQUIRED | RETRY | Additional documents required to pass the check |
AGE_REQUIREMENT_MISMATCH | FINAL | Age requirement is not met (e.g. cannot rent a car to a person below 25yo) |
EXPERIENCE_REQUIREMENT_MISMATCH | FINAL | Not enough experience (e.g. driving experience is not enough) |
CRIMINAL | FINAL | The user is involved in illegal actions |
WRONG_ADDRESS | RETRY | The address from the documents doesn't match the address that the user entered |
GRAPHIC_EDITOR | RETRY | The document has been edited by a graphical editor |
DOCUMENT_DEPRIVED | RETRY | The user has been deprived of the document |
COMPROMISED_PERSONS | FINAL | The user does not correspond to Compromised Person Politics |
PEP | FINAL | The user belongs to the PEP category |
ADVERSE_MEDIA | FINAL | The user was found in the adverse media |
FRAUDULENT_PATTERNS | FINAL | Fraudulent behavior was detected |
SANCTIONS | FINAL | The user was found on sanction lists |
NOT_ALL_CHECKS_COMPLETED | RETRY | All checks were not completed |
FRONT_SIDE_MISSING | RETRY | Front side of the document is missing |
BACK_SIDE_MISSING | RETRY | Back side of the document is missing |
SCREENSHOTS | RETRY | The user uploaded screenshots |
BLACK_AND_WHITE | RETRY | The user uploaded black and white photos of documents |
INCOMPATIBLE_LANGUAGE | RETRY | The user should upload translation of his document |
EXPIRATION_DATE | RETRY | The user uploaded expired document |
UNFILLED_ID | RETRY | The user uploaded the document without signatures and stamps |
BAD_SELFIE | RETRY | The user uploaded a bad selfie |
BAD_VIDEO_SELFIE | RETRY | The user uploaded a bad video selfie |
BAD_FACE_MATCHING | RETRY | Face check between document and selfie failed |
BAD_PROOF_OF_IDENTITY | RETRY | The user uploaded a bad ID document |
BAD_PROOF_OF_ADDRESS | RETRY | The user uploaded a bad proof of address |
BAD_PROOF_OF_PAYMENT | RETRY | The user uploaded a bad proof of payment |
SELFIE_WITH_PAPER | RETRY | The user should upload a special selfie (e.g. selfie with paper and date on it) |
FRAUDULENT_LIVENESS | FINAL | There was an attempt to bypass liveness check |
OTHER | RETRY | Some unclassified reason |
REQUESTED_DATA_MISMATCH | RETRY | Provided info doesn't match with recognized from document data |
OK | RETRY | Custom reject label |
COMPANY_NOT_DEFINED_STRUCTURE | RETRY | Could not establish the entity's control structure |
COMPANY_NOT_DEFINED_BENEFICIARIES | RETRY | Could not identify and duly verify the entity's beneficial owners |
COMPANY_NOT_VALIDATED_BENEFICIARIES | RETRY | Beneficiaries are not validated |
COMPANY_NOT_DEFINED_REPRESENTATIVES | RETRY | Representatives are not defined |
COMPANY_NOT_VALIDATED_REPRESENTATIVES | RETRY | Representatives are not validated |
APPLICANT_INTERRUPTED_INTERVIEW | RETRY | On Video Ident call user refused to finish interview |
DOCUMENT_MISSING | RETRY | On Video Ident call user refused to show or didn't have required documents |
UNSUITABLE_ENV | RETRY | On Video Ident call user is either not alone or nor visible |
CONNECTION_INTERRUPTED | RETRY | Video Ident call connection was interrupted |
UNSUPPORTED_LANGUAGE | FINAL | Unsupported language for video identification |
THIRD_PARTY_INVOLVED | FINAL | The user is doing verification from a third party for a fee |
CHECK_UNAVAILABLE | RETRY | The database is not available |
INCORRECT_SOCIAL_NUMBER | RETRY | The user provided an incorrect social number (SSN, for example) |
Finally, the reviewRejectType
field tells about the rejection type (if the RED
answer is given). The following values are possible:
Value | Description |
---|---|
FINAL | Final reject, e.g. when a person is a fraudster, or a client does not want to accept such kinds of clients in his/her system |
RETRY | Decline that can be fixed, e.g. by uploading an image of better quality |
# Example webhook payload with 'RED' answer
{
"applicantId": "5cb744200a975a67ed1798a4", // applicant ID
"inspectionId": "5cb744200a975a67ed1798a5", // applicant's inspection ID
"correlationId": "req-fa94263f-0b23-42d7-9393-ab10b28ef42d",
"externalUserId": "externalUserId",
"levelName": "basic-kyc-level",
"type": "applicantReviewed",
"reviewResult": {
// A human-readable comment that can be shown to your end user
// Please note that individual images may also contain additional document-specific comments.
// In this case this field might be empty.
// In order to get them, refer to the Getting applicant status (API)
"moderationComment": "We could not verify your profile. Please contact support: [email protected]",
// A human-readable comment that should not be shown to an end user, and is meant to be read by a client
// This field will contain applicant's top-level comments,
// plus, if the rejectType is not RETRY it may contain some private info, like that the user is a fraudster.
// we envision that this field will be used for admin areas of our clients,
// where a human can get all information
"clientComment": " Suspected fraudulent account.",
// final answer that should be highly trusted (only 'RED' and 'GREEN' are currently supported)
"reviewAnswer": "RED",
// a machine-readable constant that describes the problems in case of verification failure
"rejectLabels": ["UNSATISFACTORY_PHOTOS", "GRAPHIC_EDITOR", "FORGERY"],
"reviewRejectType": "FINAL"
},
// indicates that the verification process has been completed
// NOTE: it does not mean that the applicant was approved,
// it just means that an applicant was processed
"reviewStatus": "completed",
// time of webhook creation
"createdAtMs": "2020-02-21 13:23:19.001"
}
# Example webhook payload with 'GREEN' answer
{
"applicantId": "5cb56e8e0a975a35f333cb83",
"inspectionId": "5cb56e8e0a975a35f333cb84",
"correlationId": "req-a260b669-4f14-4bb5-a4c5-ac0218acb9a4",
"externalUserId": "externalUserId",
"levelName": "basic-kyc-level",
"type": "applicantReviewed",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"createdAtMs": "2020-02-21 13:23:19.321"
}
# Applicant life-cycle
- When creating an applicant, its status becomes
init
. - After uploading all required documents the status becomes
pending
.
Automatic status change (to pending) only works within our SDKs and when all necessary documents are uploaded.
- Within API Integration there is a special mode, when a client must manually signal to us when an applicant is ready to be reviewed.
- After the verification process is completed, the status of the applicant will change to
completed
.
# Getting applicant review status
It is recommended that you use this method if you are using WebSDK or MobileSDK since SDKs will show rejection reasons and comments within their screens. But if you still need to fetch rejection comments, it's possible using this method below.
GET /resources/applicants/{applicantId}/status
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
# RESPONSE
Name | Type | Optional | Description |
---|---|---|---|
createDate | Date | No | Date of creation of the applicant. |
reviewDate | Date | Yes | Date of check ended. |
startDate | Date | Yes | Date of check started. |
reviewResult | Object | Yes | Field that contains extra information on applicant verification results. |
reviewStatus | String | No | Current status of an applicant. |
levelName | String | Yes | Name of user verification steps configuration (level). |
attemptCnt | Integer | Yes | Number of new review attempts on the same level. |
# reviewResult
ATTRIBUTES
Name | Type | Optional | Description |
---|---|---|---|
reviewAnswer | String | No | Provide GREEN or RED to simulate answer you need. |
rejectLabels | List of Strings | Yes | Reasons for rejections. |
reviewRejectType | String | Yes | Choose FINAL or RETRY to simulate other types of rejections. |
clientComment | String | Yes | A human-readable comment that should not be shown to the end user. |
moderationComment | String | Yes | A human-readable comment that can be shown to the end user. |
buttonIds | List of Strings | Yes | A list of button IDs that were used for applicant rejection. |
The reviewStatus
can be one of the following:
Value | Description |
---|---|
init | Initial registration has started. A client is still in the process of filling out the applicant profile. Not all required documents are currently uploaded. |
pending | An applicant is ready to be processed. |
prechecked | The check is in a half way of being finished. |
queued | The checks have been started for the applicant. |
completed | The check has been completed. |
onHold | Applicant waits for a final decision from compliance officer (manual check was initiated) or waits for all beneficiaries to pass KYC in case of company verification. |
Please note that reviewAnswer
has an impact on an applicant only with reviewStatus : completed
.
# Example request
curl -X GET \
'https://api.sumsub.com/resources/applicants/5b76df770a975a1404cbcb60/status'
# Example response
{
"createDate": "2022-10-06 12:47:57+0000",
"attemptCnt": 2,
"levelName": "basic-kyc-level",
"reviewResult": {
"moderationComment": "We could not verify your profile. Please contact support: [email protected]",
"clientComment": "The submitted documents or means of payment belong to different people.",
"reviewAnswer": "RED",
"rejectLabels": ["INCONSISTENT_PROFILE", "GRAPHIC_EDITOR"],
"reviewRejectType": "FINAL",
"buttonIds" : [ "badPhoto", "badPhoto_imageEditor", "differentDocs" ]
},
"reviewStatus": "completed"
}
# Getting applicant verification steps status
It is recommended that you use this method if you'd like to fetch information about documents or separate verification step results.
Some rejection types may not affect document status, so don't forget to check overall applicant review status and answer.
GET /resources/applicants/{applicantId}/requiredIdDocsStatus
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
# RESPONSE
A breakdown for each step. Each step contains an array of images' IDs (could be several, e.g., if two sides were uploaded).
If an applicant is rejected with a FINAL
value of reviewRejectType
within webhook payload or at the answer on that endpoint, the response from requiredIdDocsStatus may not provide much useful information.
If you need to access the moderationComment or clientComment for a FINAL
rejection, it's better to retrieve that information from the webhook or applicant data payload.
To obtain images with IDs from the response, you can issue a request as described here.
# Example request:
curl -X GET \
'https://api.sumsub.com/resources/applicants/5bb8cca10a975a624903cf65/requiredIdDocsStatus'
Some steps may not have review results, it depends on the review of some other step. e.g. PROOF_OF_RESIDENCE step depends on IDENTITY step if it exists, so PROOF_OF_RESIDENCE may not have been checked until IDENTITY is approved.
# Example response:
{
"IDENTITY": {
// a step identifier
"reviewResult": {
// if exists, that means that a document was uploaded
"moderationComment": "Please upload a photo of the front of your ID card.",
"reviewAnswer": "RED"
},
"country": "LVA", // document country
"idDocType": "ID_CARD", // specific document type for the step
"imageIds": [122352326,34246467], // image IDs that represent a document
//If imageIds array contains more than one element, the first one would be front side and others - back sides
"imageReviewResults": {
"34246467": {
"reviewAnswer": "GREEN"
},
"122352326": {
"moderationComment": "Please upload a photo of the front of your ID card.", // A human-readable comment that can be shown to your end user
"clientComment": "One side of the document is missing.", //A human-readable comment that should not be shown to an end user, and is meant to be read by a client
"reviewAnswer": "RED",
"rejectLabels": ["DOCUMENT_PAGE_MISSING","FRONT_SIDE_MISSING"],
"reviewRejectType": "RETRY"
}
}
},
"SELFIE": {
"reviewResult": {
"reviewAnswer": "GREEN"
},
"country": "LVA",
"idDocType": "SELFIE",
"imageIds": [181314576],
"imageReviewResults": {
"181314576": {
"reviewAnswer": "GREEN"
}
}
}
}
# Clarifying the reason of rejection
To retrieve the rejection reasons for a document and an applicant, we assign a specific buttonId
for each rejection reason using the method provided.
When an applicant is rejected, it's possible to get exact reasons via the following request:
GET /resources/moderationStates/-;applicantId={applicantId}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
# RESPONSE
Name | Type | Optional | Description |
---|---|---|---|
imagesStates | Object | Yes | Structure contains imageId and buttonId which indicate rejection reason |
applicantState | Object | Yes | Structure contains buttonId which indicate rejection reason of applicant data or applicant itself |
imagesStates
and applicantState
are present in the response only when the applicant is being rejected.
# Available buttonId
{
"spam":[], //The applicant uploaded irrelevant images or failed to provide the required documents 6 or more times.
"redCrypto":[], //The provided e-wallet has a high risk score.
"blackCrypto":[], //The crypto wallet address is incorrect.
"differentDocs":[], //The documents or means of payment submitted by the applicant belong to different people.
"paymentsIssues":[], //The provided payment method was rejected due to high risk.
"regulationsViolations":[
"age", //The applicant does not meet the client's age requirements.
"duplicate", //By default, one applicant can have only one active account.
"wrongRegion", //The applicant is from a region or a country that is not supported by the client.
"inconsistency", //The bank card holder name does not match the name on the applicant ID.
"docNotFound", //Document data wasn't found in the databases
"countriesMismatch", //The applicant country of residence does not match the bank card registration country.
"riskBankCard", //The provided bank card has a high risk score.
"spam", //The applicant uploaded irrelevant images or failed to provide the required documents 6 or more times.
"blocklist", //We suspect the applicant account is fradulent, as it has a very high risk score.
"highRiskProfile", //The system has detected suspicious applicant behavior such as high-risk IP, using multiple devices, etc.
"gender", //The gender in the applicant profile does not match the gender in the document uploaded by the applicant.
"unsupportedLanguage" //The applicant doesn't speak any of the supported languages.
],
"dataMismatch":[
"fullName", //The full name on the profile is either missing or incorrect.
"dateOfBirth", //The date of birth on the profile is either missing or incorrect.
"address", //There was an issue with the address on the profile.
"inconsistency", //The bank cardholder name does not match the name on the ID.
"gender" //The gender in the applicant profile does not match the gender in the document uploaded by the applicant.
],
"fraudulentPatterns":[
"selfieMismatch", //Selfie does not match the picture in the document.
"fake", //Suspected fraudulent account.
"blocklist", //User account is suspected to be fake. We found it in your blocklist.
"highRiskProfile", //High behavioral risk scoring.
"physicalForgery", //Physically forged document submitted.
"wantedDocument", //Submitted document is in the list of missing/wanted.
"livenessForced", //User was forced to pass liveness.
"deepFake" //An attempt to bypass liveness check with deep fake.
"template" // Suspected tampered data.
],
"dbNetChecks":[
"incorrectTin", //The tax number provided by the applicant is incorrect.
"incorrectSsn", //The SSN number provided by the applicant is incorrect.
"incorrectBvn" //The BVN number provided by the applicant is incorrect.
],
"compromisedPersons":[
"pep", //The applicant is a politically exposed person. This means that they are currently in a position of great control or have been in the past.
"sanctionList", //The applicant was mentioned on sanctions lists.
"criminalRecords", //The applicant is a convicted criminal.
"incompleteName", //The name the applicant entered is incomplete, so the system could not match it against the watchlists properly.
"adverseMedia" //The applicant is mentioned in adverse media.
],
"company":[
"moreDocs", //Sumsub needs more documents to process with verification.
"controlStructureIsNotEstablished", //Sumsub could not establish the verified entity's control structure.
"notValidatedBeneficiaries", //Sumsub could not validate Beneficiaries.
"notDeterminedRepresentatives", //Sumsub could not validate the verified entity's representatives.
"notValidatedRepresentatives" //Sumsub could not identify or properly verify the verified entity's representatives.
],
"additionalPages":[
"mainPageId", //The uploaded document is missing the biodata page.
"nextPage", //The uploaded document is missing a page.
"registrationStamp", //The uploaded document is missing the address page.
"expirationPage", //The uploaded document is missing the expiration date page.
"proofOfAddress",
"anotherSide" //The uploaded document is double-sided and only one side was uploaded.
],
"badDocument":[
"expiredId", //The provided identity document has expired.
"invalidId", //The provided identity document is invalid.
"damagedId", //The provided ID document is damaged to the extent that it makes verification data, such as name or MRZ, unreadable (e.g. the ID is stained or scratched).
"wrongType", //The uploaded document can not be accepted for verification.
"notFullNameOrDob", //Applicant full name or date of birth is not present in the provided document.
"withoutFace", //The face of the document’s owner is poorly visible, making face match impossible.
"dataNotVisible", //The information on the uploaded photo is illegible. The image may be blurred or cropped, making it impossible to read the needed data (for example name or MRZ).
"copyOfIdDoc", //The applicant has uploaded a photo of a paper copy of their document instead of the original document, which is no acceptable.
"noStamp", //The applicant uploaded a document that does not have the stamp of the issuing organization.
"unsigned", //The applicant uploaded a document without relevant signatures and stamps.
"needTranslation", //The applicant uploaded a document in a language that is not supported by Sumsub, so the notarized English translation has been requested.
"fake", //We suspect the applicant account is fradulent.
"expirationDate", //The provided identity document expires in less than one month.
"inconsistency" //The bank card holder name does not match the name on the applicant ID.
],
"badPhoto":[
"lowQuality", //The information on the ID document is not visible or can not be read.
"editedPoa", //The uploaded photo displays signs of tampering via a digital editor.
"screenshot", //The applicant uploaded screenshots instead of photos or scans.
"blackAndWhite", //The applicant uploaded black and white photos of a document instead of color photos.
"imageEditor", //The photo displays signs of tampering via a digital editor. The purpose of the editor could be anything from resizing to forgery.
"incomplete", //Part of the document (document corners) is not visible.
"dataNotVisible", //The information on the uploaded photo is illegible. The image may be blurred or cropped, making it impossible to read the needed data (for example name or MRZ).
"sticker" //The uploaded photo displays signs of tampering via a digital editor.
],
"selfie":[
"lowQuality", //The information on the ID document is not visible or can not be read.
"selfieWithId", //The applicant uploaded a selfie without the ID document.
"webcamSelfie", //The applicant failed the Liveness check.
"selfieWithAnotherId", //The ID document in the selfie is different from the ID document uploaded by the applicant.
"selfieWithNewId", //The ID document in the selfie can not be detected or read.
"blackAndWhite", //The applicant uploaded black and white photos of a document instead of color photos.
"badFaceComparison", //The applicant's face on the selfie is not clearly visible and can not be matched against the photo in the ID document.
"selfieLiveness", //The applicant failed the Liveness check.
"livenessWatermark" //There is camera text or a watermark on the Liveness record originating from the proprietary software.
],
"videoSelfie":[
"lowQuality", //The information on the ID document is not visible or can not be read.
"videoWithoutFace", //The pronounced phrase does not match the provided text.
"spokenPhraseMismatch" //The applicant's face can not be detected on the selfie.
],
"proofOfAddress":[
"fullName", //The full name of the applicant has not been provided on the document or can’t be read.
"fullAddress", //The full address of the applicant has not been provided on the document or can’t be read.
"issueDate", //There is no issue date on the document uploaded by the applicant.
"listOfDocs", //The applicant uploaded a document that is not supported by Sumsub.
"expirationDate", //The provided identity document expires in less than one month.
"dataMismatch", //The address on the document uploaded by the applicant does not match the address entered by the applicant.
"sameDoc", //The applicant uploaded the same document as PoA and PoI.
"certifiedForm", //There is no stamp/signature of the issuing organization on the document uploaded by the applicant.
"notEnoughData" //The account/card number on the bank statement is missing or can not be read.
],
"proofOfPayment":[
"bankCard", //The information on the bank card is missing or can’t be read.
"bankStatement", //The information on the bank statement is missing or can’t be read.
"e-wallet", //The e-wallet needs to be confirmed.
"wireTransfer", //The wire transfer needs to be confirmed.
"bankAccount", //The information about the bank account is missing.
"dataMismatch" //The address on the document uploaded by the applicant does not match the address entered by the applicant.
],
"bankCard":[
"fullNameIssue", //The name on the bank card is missing.
"expirationDate", //The provided bank card has expired or expires soon.
"noCard" //The applicant uploaded an image of a copy of the bank card.
],
"bankStatement":[
"fullNameIssue", //The full name on the bank statement is missing or can not be read.
"numberMismatch", //The bank card number on the bank statement does not match the provided bank card.
"invalidIssueDate", //The issue date on the bank statement is missing or can not be read.
"notEnoughData" //The account/card number on the bank statement is missing or can not be read.
],
"fake":[
"editedMrz", //MRZ of the document was modified.
"editedId", //Data in the document was modified.
"forgedId", //The document has been physically tampered with.
"webId", //The photo of the document was found on the dark web.
"fakePoa", //The information in the PoA document is inconsistent.
"editedBarcode", //The barcode in the document was modified.
"fakeLiveness", //An attempt to bypass the liveness check was detected.
"fakeSelfie", //The ID document used in the selfie is fake.
"nfcMismatch" //The data extracted form the NFC chip does not match the data in the document.
],
"selfieIssues":[
"livenessWithPhone", //The applicant phone was present during the livdeness check.
"livenessDifferentPeople" //Multiple people/devices were present during the liveness check.
],
"videoIdent":[
"notAlone", //On the Video Ident call, the applicant was not alone.
"badConnection", //The Video Ident call connection was interrupted.
"notSeen", //On the Video Ident call, the applicant was not visible.
"noSuitableDocs", //The applicant failed to provide the required documents on the Video Ident call.
"applicantInterruptedInterview" //On the Video Ident call, the applicant did not finish the interview.
],
"videoIdentFinalRejection":[
"unsupportedLanguage", //The applicant doesn't speak any of the supported languages.
"3rdPartyInvolvement", //We suspect the applicant was paid to create the account.
"forcedVerification" //The video indent call showed that the applicant was misled and/or forced by the 3rd party to create the account.
],
"ekycReject":[
"notEnoughDataInSource", //Sumsub could not find the applicant data in the government database.
"skip", //E-KYC step has been skipped.
"dataMismatch" //The data provided by the applicant does not match the data from the government database.
],
"ekycRetry":[
"checkUnavailable" //The government database is currently unavailable, so we were not able to verify applicant data.
]
}
# Example request
curl -X GET \
'https://api.sumsub.com/resources/moderationStates/-;applicantId=5eeb914604f940217bdbad05'
# Example response
{
"list": {
"items": [
{
"id": "5eebac3dd5ea480a98d51b40",
"applicantId": "5eeb914604f940217bdbad05",
"key": "TFRRN1THCATLDW",
"imagesStates": {
"104037333": { //imageId
"state": {
"badPhoto": { //Unsatisfactory photo was submitted
"value": true //buttonId is active
},
"badPhoto_imageEditor": { //Graphic editing software usage was detected
"value": true
}
}
},
"885188054": {
"state": {
"badPhoto": {
"value": true
},
"badPhoto_imageEditor": {
"value": true
}
}
},
"1777063415": {
"state": {
"badPhoto": { //Unsatisfactory photo was submitted
"value": true
},
"badPhoto_incomplete": { //Not all document edges are visible
"value": true
}
}
},
"1054210547": {
"state": {
"badPhoto": {
"value": true
},
"badPhoto_incomplete": {
"value": true
}
}
},
"1234193281": {
"state": {
"badPhoto": {
"value": true
},
"badPhoto_incomplete": {
"value": true
}
}
},
"484169174": {
"state": {
"badPhoto": {
"value": true
},
"badPhoto_imageEditor": {
"value": true
}
}
},
"741138243": {
"state": {
"badPhoto": {
"value": true
},
"badPhoto_incomplete": {
"value": true
}
}
},
"2118922684": {
"state": {
"badPhoto": {
"value": true
},
"badPhoto_imageEditor": {
"value": true
}
}
},
"1719337639": {
"state": {
"badPhoto": {
"value": true
},
"badPhoto_incomplete": {
"value": true
}
}
}
},
"applicantState": {
"regulationsViolations_wrongRegion": { //User is from excluded region/country
"value": false //buttonId is inactive
},
"regulationsViolations": {
"value": false
},
"dataMismatch": {},
"dataMismatch_address": {
"value": false
},
"regulationsViolations_duplicate": {
"value": false
}
}
}
],
"totalItems": 1
}
}
# Requesting an applicant check
You can programmatically ask us to re-check an applicant in cases where you or your user believe that our system made a mistake,
or you're sending us documents via API and would like us to perform a check. To initiate a re-check, simply move the applicant to the pending
state by performing the following request.
POST /resources/applicants/{applicantId}/status/pending?reason={reason}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
reason | String | No | A note indicating the reason for checking the applicant profile |
# RESPONSE
In case of HTTP response 200
, you may ignore the response body.
# Example request
curl -X POST \
'https://api.sumsub.com/resources/applicants/5b73b82f0a975a3b46429758/status/pending?reason=someReason'
# Example response
{
"ok": 1
}
# Requesting an applicant check with different verification type
If you plan to use this endpoint, please inform the Sum&Substance team, because this requires certain settings on our side.
You can request applicant check with flow that differs from your usual checks.
POST /resources/applicants/{applicantId}/status/pending?reasonCode={reasonCode}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
reasonCode | String | Yes | Code of the new flow |
Provide us a reasonCode, so we could set up different flows on our side.
# RESPONSE
In case of HTTP response 200
, you may ignore the response body.
# Example request
curl -X POST \
'https://api.sumsub.com/resources/applicants/5b73b82f0a975a3b46429758/status/pending?reasonCode=wlCheck'
# Example response
{
"ok": 1
}
# Getting document images
Since a user may re-upload images several times and, for example, also change a passport photo to an ID card, it might be tricky for you to understand which ones actually made the applicant pass or fail.
If you are interested in receiving images that were part of the final verification, you should use this method.
GET /resources/inspections/{inspectionId}/resources/{imageId}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
inspectionId | String | Yes | Inspection ID (it's a part of an applicant) |
imageId | String | Yes | Image ID (see above) |
# RESPONSE
Binary content representing a document. The Content-Type
response header precisely describes the response mime-type. The full list of mime-types and file formats you can find here.
# Example request:
curl -X GET \
'https://api.sumsub.com/resources/inspections/5bb8cca10a975a624903cf66/resources/551262049'
# Adding an applicant to blocklist
If for some reason you need to add an applicant to the blocklist, you can use this endpoint. It is necessary to add the reason for adding the applicant to the blocklist.
POST /resources/applicants/{applicantId}/blacklist
We advise checking if the applicant is not already in our blocklist first (check for BLACKLIST
and BLOCKLIST
values within rejectLabels[]
array of that applicant). In case you'll try to add a blocklisted applicant to blocklist again we'll throw a 400
http status with 5000
error code.
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
note | String | Yes | Reason or note for the applicant added to the blocklist (URL encoded) |
# RESPONSE
The answer will contain the applicant added to the blocklist.
# Example request
curl -X POST \
'https://api.sumsub.com/resources/applicants/5c0e93c30a975a53a79aa54b/blacklist?note=A%20user%20provided%20a%20fake%20document'
# Example response
{
"id": "5bb8563c0a975a5e6b322ecc",
"createdAt": "2018-10-06 06:29:16",
"clientId": "SumsubClient",
"inspectionId": "5bb8563c0a975a5e6b322ecd",
"externalUserId": "someClientUserId",
"info": {
"firstName": "John",
"lastName": "Smith",
"dob": "2000-03-04",
"placeOfBirth": "London",
"country": "GBR",
"phone": "+449112081223"
},
"requiredIdDocs": {
"docSets": [
{
"idDocSetType": "IDENTITY",
"types": ["PASSPORT", "ID_CARD", "DRIVERS"]
}
]
},
"review": {
"createDate": "2018-12-10 16:26:45+0000",
"reviewDate": "2018-12-10 16:27:29+0000",
"reviewResult": {
"reviewAnswer": "RED",
"rejectLabels": ["BLOCKLIST"],
"reviewRejectType": "FINAL"
},
"reviewStatus": "completed"
}
}
# Sharing applicants between partner services
Sometimes our clients partner with each other and want to simplify and speed up verification for their users when they have passed KYC once via Sum&Substance.
Assume a user A
passed KYC in service X
, and is now registering at the partner service Y
. If X agrees to share information about A
with Y
, it can be done as follows:
X
generates ashare token
and passes it toY
Y
calls our API with theshare token
and receives an applicant for userA
(with all its data and documents) on their account
Contact our team before using data sharing functionality in order to sign tripartite agreement on personal data sharing with you, Sumsub and your partner service.
# Generating a share token
POST /resources/accessTokens/-/shareToken?applicantId={applicantId}&forClientId={clientId}
# Example of getting a share token (done by X
)
## applicantId and Y's client ID must be provided
curl -X POST \
'https://api.sumsub.com/resources/accessTokens/-/shareToken?applicantId=5ce412012b4da877b2d910bd&forClientId=CoolCoinLtd'
# Example response
{
"token": "eyJhbGciOiJub25lIn0.eyJqdGkiOiJfYWN0LTZmODI2ZTU0LTE2MzctNDViMS05NzMyLWY1MjZiN2YxNWE3YyIsInVybCI6Imh0dHBzOi8vYXBpLnN1bXN1Yi5jb20ifQ.",
"forClientId": "CoolCoinLtd"
}
Make sure your integration code does not validate or analyze the access token content, as the format is not fixed and may undergo further changes in the future. The token must be treated as an arbitrary string with the maximum length of 1KB.
# REQUEST ARGUMENTS
Name | Type | Required | Description | Default |
---|---|---|---|---|
applicantId | String | Yes | Applicant ID on service X | |
forClientId | String | Yes | Client ID of service Y (ask them for this information). You can find your clientId at the dashboard in the applicant profile - field of "Created for" and at response of getting applicant data request - clientId | |
ttlInSecs | Integer | No | Time to live in seconds | 1200 |
# RESPONSE
Share token in the token
field of the returned JSON (see example).
# Importing an applicant
# Example of importing an applicant (done by Y
)
curl -X POST \
'https://api.sumsub.com/resources/applicants/-/import?shareToken=_act-0b8a43f6-b70f-4ad3-bda9-7ce904589380'
# Example response
{
// Applicant in service Y
"id": "5d08a63239b79354a2ebaa1d",
"createdAt": "2019-06-18 10:52:02",
"clientId": "CoolCoinLtd",
...
}
POST /resources/applicants/-/import?shareToken={shareToken}
# REQUEST ARGUMENTS
Name | Type | Required | Description | Default |
---|---|---|---|---|
shareToken | String | Yes | Share token generated by X | |
resetIdDocSetTypes | String | No | Specify one or few comma-separated document types if an applicant has to resubmit those documents to be verified. Examples, SELFIE , IDENTITY , etc. | |
trustReview | Boolean | No | If you trust your partner's check result, then you should use true . If it is false , then the applicant will be rechecked. | false |
userId | String | No | Sets your own externalUserId for the imported applicant. In case of empty value we'll generate a random one. | |
levelName | String | No | Sets specified levelName to imported applicant and sets init in case not all required documents present. |
# RESPONSE
Applicant entity in service Y
. Note a new applicant ID will be returned to the response. Bind it in your system as needed.
Please note that share tokens will be invalidated once used.
# Resetting a single verification step
For some cases, it's required for user to pass an already passed verification step - the method below will make the step inactive for SDK to run it again and collect new data.
Please note that not all verification steps are possible to reset using the method below.
POST /resources/applicants/{applicantId}/resetStep/{idDocSetType}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
idDocSetType | String | Yes | Step name to reset |
# AVAILABLE STEPS TO RESET
Name | Description |
---|---|
PHONE_VERIFICATION | Phone verification step |
EMAIL_VERIFICATION | Email verification step |
QUESTIONNAIRE | Questionnaire |
APPLICANT_DATA | Applicant data |
IDENTITY | Identity step |
IDENTITY2 | 2nd Identity step |
IDENTITY3 | 3rd Identity step |
IDENTITY4 | 4th Identity step |
PROOF_OF_RESIDENCE | Proof of residence |
PROOF_OF_RESIDENCE2 | 2nd Proof of residence |
SELFIE | Selfie step |
SELFIE2 | 2nd Selfie step |
# Example request
curl -X POST \
'https://api.sumsub.com/resources/applicants/62164ddf8a86e20bbd28ab75/resetStep/PHONE_VERIFICATION'
# Example response
{
"ok": 1
}
# Resetting an applicant
In very rare cases, it is required to change the status of the applicant to init
. For example, if a user has contacted support with a request to re-pass verification from scratch with new documents.
Please note that resetting an applicant with fraudulent patterns is not a safe option.
You can make sure that it's safe to reset by checking if applicant does not have any of these rejectLabels
: FORGERY
, SELFIE_MISMATCH
, BLACKLIST
, BLOCKLIST
, INCONSISTENT_PROFILE
, FRAUDULENT_PATTERNS
.
If you plan to use this endpoint, please inform the Sum&Substance team, as we may be able to suggest another flow for your case.
POST /resources/applicants/{applicantId}/reset
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
# Example request
curl -X POST \
'https://api.sumsub.com/resources/applicants/5d6f821b0a975a0b8aa27b27/reset'
# Example response
{
"ok": 1
}
# Changing top-level info
This method patches top-level applicant info like email, externalUserId, sourceKey, type for specified applicantId
. The body must contain only those fields that you intend to change. Null fields will be ignored.
PATCH /resources/applicants/
# REQUEST BODY
Name | Type | Required | Description |
---|---|---|---|
id | String | Yes | Applicant ID |
externalUserId | String | No | New External ID for an applicant |
String | No | New email | |
phone | String | No | New phone number |
sourceKey | String | No | New sourceKey |
lang | String | No | The language in which the applicant should see result of verification |
questionnaires | Array of objects | No | Questionnaire configuration with answers |
metadata | Array of objects | No | Additional information about user at custom fields |
# Example request
curl -X PATCH \
'https://api.sumsub.com/resources/applicants' \
-H 'Content-Type: application/json' \
-d '{"id":"5e9ad53d0a975a656d67e4d0","externalUserId":"1983114","email":"[email protected]"}'
# Example response
{
"id": "5e9ad53d0a975a656d67e4d0",
"createdAt": "2020-04-18 10:23:57",
"inspectionId": "5e9ad53d0a975a656d67e4d1",
"externalUserId": "1983114",
"email": "[email protected]",
"requiredIdDocs": {
"docSets": [
{
"idDocSetType": "IDENTITY",
"types": [
"ID_CARD",
"PASSPORT",
"DRIVERS"
]
},
{
"idDocSetType": "SELFIE",
"types": [
"SELFIE"
],
"videoRequired": "liveness"
}
]
},
"review": {
"createDate": "2020-04-18 10:25:53+0000",
"expireDate": "2020-04-18 10:40:53+0000",
"reviewStatus": "init"
},
"lang": "en",
"type": "individual"
}
# Deactivating applicant profile
In some cases, you may need to deactivate an applicant, so no action could be performed with the applicant profile, and all applicant data from this profile will be ignored when checking new applicants for duplicates. You can later reactivate the profile by specifying the activated
status in the request.
To deactivate/reactivate the applicant profile, use the following method:
PATCH /resources/applicants/{applicantId}/presence/{status}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID. |
status | String | Yes | Possible values:
|
# Example request
curl -L -g -X PATCH \
'https://api.sumsub.com/resources/applicants/64f6062643d7305937748bf2/presence/deactivated'
The response would include the array of objects containing the applicant data. The set of data depends on the permissions you have in the system.
Mind the “deleted” parameter value which indicates the request result:
true
— applicant profile is deactivated.false
— applicant profile is activated.
# Example response
{
"id" : "652dsf45fg563798cee64d3e",
"createdAt" : "2023-10-16 13:34:54",
"key" : "WBFHTKYHGUTOLX",
"clientId" : "john.doe",
"inspectionId" : "652deh58gk68df29cee64d3f",
"externalUserId" : "dash-315bcbed-eadd-4f50-a5f4-csge467fht269",
"applicantPlatform" : "API",
"requiredIdDocs" : {
"videoIdent" : true,
"videoIdentUploadTypes" : [ "UTILITY_BILL" ],
"docSets" : [ {
"idDocSetType" : "IDENTITY",
"types" : [ "ID_CARD", "PASSPORT" ]
}, {
"idDocSetType" : "PROOF_OF_RESIDENCE",
"types" : [ "UTILITY_BILL" ],
"poaStepSettingsId" : "64ad4c234568fghj28f496ad"
} ]
},
"deleted" : true,
"review" : {
"levelName" : "vi_old",
"createDate" : "2023-10-16 13:39:34+0000",
"startDate" : "2023-10-16 13:39:47+0000",
"reviewStatus" : "init"
},
"lang" : "en",
"type" : "individual"
}
# Import applicants
This method is intended for data import. For example, in case you already got a lot of applicants and want to recheck or maintain data consistency.
If you plan to use this endpoint, please inform the Sum&Substance team, as we may be able to suggest another flow for your case.
POST /resources/applicants/-/ingestCompleted?levelName={levelName}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
#{body} | Object | Yes | An object representing an applicant (see example). |
levelName | String | No | Name of user verification steps configuration from the dashboard. |
Please note that the request body structure may be unique depending on the case, please inform the Sum&Substance team before using this method.
# Example request
curl -X POST 'https://api.sumsub.com/resources/applicants/-/ingestCompleted?levelName=basic-kyc-level' \
-H 'Content-Type: application/json' \
-d '{
"externalUserId": "someClientUserId",
"info": {
"firstName": "John",
"lastName": "Snow",
"country": "GBR"
},
"review": {
"elapsedSincePendingMs": 100,
"elapsedSinceQueuedMs": 50,
"createDate": "2022-11-21 08:25:32+0000",
"reviewDate": "2022-11-21 08:25:36+0000",
"reviewResult": {
"moderationComment": "",
"clientComment": "",
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed"
}
}'
# Example response
{
"id": "5f0864923cc18175d35bf1aa",
"createdAt": "2020-07-10 12:52:34",
"key": "QJZKBRIYUSBBWY",
"clientId": "yourclientid",
"inspectionId": "5f0864923cc18175d35bf1ab",
"externalUserId": "someClientUserId",
"info": {
"firstName": "John",
"lastName": "Snow"
},
"requiredIdDocs": {
"docSets": [{
"idDocSetType": "APPLICANT_DATA"
}]
},
"review": {
"elapsedSincePendingMs": 100,
"elapsedSinceQueuedMs": 50,
"createDate": "2022-11-21 08:25:32+0000",
"reviewDate": "2022-11-21 08:25:36+0000",
"reviewResult": {
"moderationComment": "",
"clientComment": "",
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"levelName": "basic-kyc-level"
},
"lang": "en",
"type": "individual"
}
# Import applicant with images by archive
This method is intended for applicant data and images import with its review results. Archive will be processed and as a result an applicant with approved documents within it will be created.
POST /resources/applicants/-/applicantImport
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
#{body} | Binary | Yes | Zip archive with data to import. |
Within the request body zip archive should be sent. For the service to be able to process an archive it should contain
applicant.json
file that has applicant entry with review, similar to the body for request above- directories, that contain an image to upload and
applicantIdDoc.json
file with info on the document (one per directory)

You can find archive example here.
# Example request
curl -X POST 'https://api.sumsub.com/resources/applicants/-/applicantImport' \
-H 'Content-Type: application/zip' \
-d '@/SumSub/Example/applicant.zip'
# Example applicant.json file content
Make sure to specify .review.levelName
to create applicant with a list of required documents.
{
"externalUserId": "someClientUserId",
"info": {
"firstName": "John",
"lastName": "Snow",
"country": "GBR"
},
"review": {
"createDate": "2022-11-21 08:25:32+0000",
"reviewDate": "2022-11-21 08:25:36+0000",
"levelName": "basic-kyc-level",
"reviewResult": {
"moderationComment": "",
"clientComment": "",
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed"
}
}
# Example applicantIdDoc.json file content
applicantIdDoc.json resemble the same object that is used as metadata
to upload an image to the applicant.
Object values of idDocType
and country
should always present within applicantIdDoc.json file.
{
"idDocType": "PASSPORT",
"country": "GBR"
}
# Example response
{
"id": "5f0864923cc18175d35bf1aa",
"createdAt": "2020-07-10 12:52:34",
"key": "QJZKBRIYUSBBWY",
"clientId": "yourclientid",
"inspectionId": "5f0864923cc18175d35bf1ab",
"externalUserId": "someClientUserId",
"info": {
"firstName": "John",
"lastName": "Snow"
},
"requiredIdDocs": {
"docSets": [{
"idDocSetType": "APPLICANT_DATA"
}]
},
"review": {
"elapsedSincePendingMs": 100,
"elapsedSinceQueuedMs": 50,
"createDate": "2022-11-21 08:25:32+0000",
"reviewDate": "2022-11-21 08:25:36+0000",
"reviewResult": {
"moderationComment": "",
"clientComment": "",
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"levelName": "basic-kyc-level"
},
"lang": "en",
"type": "individual"
}
# Set risk level for the applicant
This method allows you to set a risk level for your applicant by your own criteria.
POST /resources/applicants/{applicantId}/riskLevel/entries
Please note that App Token should have special permission to make this request (contact the Sum&Substance tech team).
# REQUEST BODY
Name | Type | Required | Supported values |
---|---|---|---|
comment | String | Yes | Any string |
riskLevel | String | Yes | unknown /low /medium /high |
# Example request
curl -X POST 'https://api.sumsub.com/resources/applicants/5edbe3460a975a259dd2f993/riskLevel/entries' \
-H 'Content-Type: application/json' \
-d '{"comment":"Danger!","riskLevel":"high"}'
# Example response
{
"riskLevel": "high",
"entries": [
{
"id": "5eda11960a975a6de48b5c08",
"createdAt": "2020-06-05 09:34:14",
"comment": "Danger!",
"riskLevel": "high"
}
]
}
# Adding custom applicant tags
Use this method to assign custom tags to applicant profiles. Also, create new tags in the Global settings section of the dashboard
POST /resources/applicants/{applicantId}/tags
# REQUEST ARGUMENTS
Name | Type | Required | Supported values |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
#{body} | Array | Yes | Array of custom tags strings |
# Example request
curl -X POST 'https://api.sumsub.com/resources/applicants/5fd1012c885b5d0009d50e6e/tags' \
-H 'Content-Type: application/json' \
-d '["Approved Sanctions", "Approved PEP", "821"]'
# Example response
{
"ok": 1
}
# Marking image as inactive (deleted)
This method allows you to mark uploaded image as deleted so during initialization SDK screen would ask for a new one. It can be used in cases you want your users to re-upload document that was previously approved via SDK.
Note, that trying to mark images as inactive while the applicant is being processed at the moment (pending
, prechecked
, queued
statuses) will result in error and 304
HTTP status.
DELETE /resources/inspections/{inspectionId}/resources/{imageId}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
inspectionId | String | Yes | Inspection id |
imageId | String | Yes | Image ID (more info here) |
# QUERY PARAMETERS
Name | Type | Required | Default | Description |
---|---|---|---|---|
revert | Boolean | No | false | Set true to revert inactivity of the image |
# RESPONSE
In case of HTTP response 200
, you may ignore the response body.
# Example request
curl -X DELETE \
'https://api.sumsub.com/resources/inspections/5f649369aee05c75ea54c80a/resources/1045510331'
# Example response
{
"ok": 1
}
# Setting review for applicant on the sandbox
On a sandbox you can try receiving results
via applicantReviewed
webhooks by triggering the following endpoint
(provide either GREEN
or RED
to simulate a positive or negative answer, correspondingly):
POST /resources/applicants/{applicantId}/status/testCompleted
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
#{body} | Object | Yes | Please see example |
applicantId | String | Yes | Applicant ID |
# REQUEST BODY
Name | Type | Required | Description |
---|---|---|---|
reviewAnswer | String | Yes | Provide GREEN or RED to simulate answer you need |
rejectLabels | Array of Strings | Yes | Reasons for rejections |
reviewRejectType | String | No | Choose FINAL or RETRY to simulate other types of rejections |
clientComment | String | No | A human-readable comment that should not be shown to the end user |
moderationComment | String | No | A human-readable comment that can be shown to the end user |
# RESPONSE
In the case of HTTP response 200
, you may ignore the response body.
# Example request to change the applicant status to GREEN
curl -X POST \
'https://api.sumsub.com/resources/applicants/5bb8cca10a975a624903cf65/status/testCompleted' \
-H 'content-type: application/json' \
-d '{
"reviewAnswer" : "GREEN",
"rejectLabels": []
}
'
# Example request to change the applicant status to RED
curl -X POST \
'https://api.sumsub.com/resources/applicants/5bb8cca10a975a624903cf65/status/testCompleted' \
-H 'content-type: application/json' \
-d '{
"reviewAnswer": "RED",
"moderationComment": "We do not accept screenshots. Please upload an original photo.",
"clientComment": "Screenshots are not accepted.",
"reviewRejectType": "RETRY",
"rejectLabels": ["UNSATISFACTORY_PHOTOS","SCREENSHOTS"]
}
'
# Example response
{
"ok": 1
}
# Webhooks
A webhook is an HTTP POST request sent to a target you define at Dev space dashboard.
Please note that we do not send any info to endpoints using http, only https.
Not receiving an answer from webhook endpoint for more than 15 seconds will be considered as a timeout, and our service will try to resend that webhook again later.
Supported versions of TLS protocol are 1.2 or higher.
The Sandbox does not automatically check the applicants. If you need to change the status of the applicant or get webhook on the test server, you can do it by yourself.
Please note that there is a limit on the webhook number: 20 webhook objects max.
We can send webhooks not only via HTTP request, but also as notifications in Slack, Telegram or via email.
In case for some reason you missed the webhooks, don't worry: we make a note of everything that was attempted to be sent, and can resend failed webhooks at any point in time. When webhook http request fails, we attempt to resend it four times: after 5 minutes, 1 hour, 5 and 18 hours until request succeeds.
Check createdAtMs
field of webhook payload to make sure that you're getting relevant applicant status.
# Webhook types
The types of webhooks we send depend on the settings.
Value | Description |
---|---|
applicantReviewed | When verification is completed. Contains the verification result. More information about this type of webhook can be found here. |
applicantPending | When a user uploaded all the required documents and the applicant's status changed to pending. |
applicantCreated | When an applicant is created. |
applicantOnHold | Processing of the applicant is paused for an agreed reason. |
applicantPersonalInfoChanged | Applicant's personal info has been changed. |
applicantPrechecked | When primary data processing is completed. |
applicantDeleted | Applicant has been permanently deleted. |
applicantLevelChanged | Applicant level has been changed. |
videoIdentStatusChanged | Status of Video Ident type of verification has been changed. |
applicantReset | Applicant has been reset: applicant status changed to init and all documents were set as inactive. You can find more info here. |
applicantActionPending | Applicant action status changed to pending. More info about applicant actions you may find here. |
applicantActionReviewed | Applicant action verification has been completed. More info about applicant actions you may find here. |
applicantActionOnHold | Applicant action verification has been paused for an agreed reason. More info about applicant actions you may find here. |
applicantWorkflowCompleted | Workflow has been completed for an applicant. |
Example applicantReviewed webhook payload you can find here.
# Webhook payload attributes
Name | Type | Optional | Description |
---|---|---|---|
applicantId | String | No | Applicant ID. |
inspectionId | String | No | Inspection ID (it's a part of an applicant). |
correlationId | String | No | This id uniquely identifies an event on our side. |
levelName | String | Yes | Applicant level name. |
externalUserId | String | No | Unique user Id on your side. |
type | String | No | Webhook type. |
sandboxMode | Boolean | No | True if the webhook was sent from the Sandbox mode. |
reviewStatus | String | No | Current status of an applicant. More info can be found here. |
createdAtMs | Date | No | Date and time of webhook creation (format yyyy-MM-dd HH:mm:ss.fff , e.g. 2021-05-14 16:00:25.032) in UTC. |
applicantType | String | Yes | Type of the applicant e.g. company /individual . |
reviewResult | Object | Yes | Field that contains extra information on applicant verification results. |
applicantMemberOf | Array of objects | Yes | Contains the list of company applicantIds that current applicant belongs as a beneficiary. |
videoIdentReviewStatus | String | Yes | Status of the videoIdent call. |
applicantActionId | String | Yes | Id of an applicant action. |
externalApplicantActionId | String | Yes | Unique action Id on your side. |
clientId | String | No | Unique identifier of you as our client. |
We don't send any personal data via webhooks. You can fetch all recognized data using these methods via API.
# Example applicantCreated webhook payload:
{
"applicantId": "5c9e177b0a975a6eeccf5960",
// inspection ID that contains a result
"inspectionId": "5c9e177b0a975a6eeccf5961",
// an ID to debug in case of unexpected errors (should be provided to Sum&Substance)
"correlationId": "req-63f92830-4d68-4eee-98d5-875d53a12258",
"levelName": "basic-kyc-level",
"externalUserId": "12672",
// type of webhook (see the corresponding section)
"type": "applicantCreated",
"sandboxMode": "false",
"reviewStatus": "init",
"createdAtMs": "2020-02-21 13:23:19.002",
"clientId": "SumsubClient"
}
# Example applicantPending webhook payload:
{
"applicantId": "5c7791f80a975a1df426b9e9",
"inspectionId": "5c7791f80a975a1df426b9ea",
"applicantType" : "individual",
"correlationId": "req-4af54c06-6a50-4cb9-a7dc-b94b2f5b07eb",
"levelName": "liveness-level",
"externalUserId": "12672",
"type": "applicantPending",
"sandboxMode": "false",
"reviewStatus": "pending",
"createdAtMs": "2020-02-21 13:23:19.002",
"clientId": "SumsubClient"
}
# Example applicantOnHold webhook payload:
{
"inspectionId": "5d10ca4e0a975a1c4cc30bbb",
"applicantType" : "individual",
"correlationId": "req-a98abc30-a5d9-4e1d-bab4-2f1af64bd5a5",
"levelName": "poa-level",
"externalUserId": "12672",
"reviewStatus": "onHold",
"applicantId": "5d10ca4e0a975a1c4cc30bba",
"type": "applicantOnHold",
"sandboxMode": "true",
"createdAtMs": "2020-02-21 13:23:19.002",
"clientId": "SumsubClient"
}
# Example applicantPersonalInfoChanged webhook payload:
{
"applicantId" : "5ede51230a975a19a19ba5c1",
"inspectionId" : "5ede51230a975a19a19ba5c2",
"applicantType" : "individual",
"correlationId" : "req-60103dee-79f1-43f4-bdcc-eb2554556afa",
"levelName": "id+liveness",
"externalUserId" : "12672",
"type" : "applicantPersonalInfoChanged",
"sandboxMode": "false",
"reviewResult" : {
"reviewAnswer" : "GREEN"
},
"reviewStatus" : "completed",
"createdAtMs" : "2020-06-08 19:39:29.002",
"clientId": "SumsubClient"
}
# Example applicantPrechecked webhook payload:
{
"applicantId": "5d1f2914c2d75a1c14130bd2",
"inspectionId": "5d1f2914c2d75a1c14130bd3",
"applicantType" : "individual",
"correlationId": "req-e9d77142-59e6-4713-9b07-9b342cc51dda",
"levelName": "kyc",
"externalUserId": "12672",
"type": "applicantPrechecked",
"sandboxMode": "false",
"reviewStatus": "queued",
"createdAtMs": "2020-02-21 13:23:19.002",
"clientId": "SumsubClient"
}
# Example applicantDeleted webhook payload:
{
"applicantId": "5f194e74040c3f316bda271c",
"inspectionId": "5f194e74040c3f316bda271d",
"applicantType": "individual",
"correlationId": "req-d34c974c-5935-41b8-a0a9-cedd2407eada",
"levelName": "phone-level",
"externalUserId": "12672",
"type": "applicantDeleted",
"sandboxMode": "false",
"reviewStatus": "init",
"createdAtMs": "2020-07-23 11:18:33.002",
"clientId": "SumsubClient"
}
# Example applicantLevelChanged webhook payload:
{
"applicantId": "5f194e74040c3f316bda271c",
"inspectionId": "5f194e74040c3f316bda271d",
"applicantType": "individual",
"correlationId": "req-d34c974c-5935-41b8-a0a9-cedd2407eadd",
"levelName": "basic-kyc-level",
"externalUserId": "12672",
"type": "applicantLevelChanged",
"sandboxMode": "false",
"reviewStatus": "init",
"createdAtMs": "2020-07-23 11:19:33.002",
"clientId": "SumsubClient"
}
# Example videoIdentStatusChanged webhook payload when video call has been initialized:
{
"applicantId": "5f8993f93324610009e5885e",
"inspectionId": "5f8993f93324610009e5885f",
"applicantType": "individual",
"correlationId": "req-5a570d5d-e27c-484b-94a9-0f1e9fb2764c",
"levelName": "kyc_video",
"externalUserId": "flow-feb1f7ad-b2e2-479a-a4d5-bca5486f7b85",
"type": "videoIdentStatusChanged",
"sandboxMode": "false",
"reviewStatus": "init",
"videoIdentReviewStatus": "pending",
"createdAtMs": "2020-10-16 12:37:26.002",
"clientId": "SumsubClient"
}
# Example videoIdentStatusChanged webhook payload when video call has been completed:
{
"applicantId": "596928690a975a137c5b207e",
"inspectionId": "596928690a975a137c5b207a",
"correlationId": "req-a7812631-2c40-4f13-8407-fd42a516f0b0",
"levelName": "basic-kyc",
"type": "videoIdentStatusChanged",
"sandboxMode": "false",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"videoIdentReviewStatus": "pending",
"createdAtMs": "2020-10-16 12:34:25.003",
"clientId": "SumsubClient"
}
# Example applicantReset webhook payload:
{
"applicantId": "5f194e74040c3f316bda271c",
"inspectionId": "5f194e74040c3f316bda271d",
"applicantType": "individual",
"correlationId": "req-57fed49a-07b8-4413-bdaa-a1be903769e9",
"levelName": "basic-kyc-level",
"externalUserId": "12672",
"type": "applicantReset",
"sandboxMode": "false",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "init",
"createdAtMs": "2021-03-01 11:34:51.104",
"clientId": "SumsubClient"
}
# Example applicantWorkflowCompleted webhook payload:
{
"applicantId": "64106d6b7d5a2d5159e6b01a",
"inspectionId": "64106d6b7d5a2d5159e6b01b",
"applicantType": "individual",
"correlationId": "req-57fed49a-07b8-4413-bdaa-a1be903769e9",
"levelName": "basic-kyc-level",
"sandboxMode": false,
"externalUserId": "12672",
"type": "applicantWorkflowCompleted",
"reviewResult": {
"reviewAnswer": "RED",
"rejectLabels": [
"AGE_REQUIREMENT_MISMATCH"
],
"reviewRejectType": "FINAL",
"buttonIds": []
},
"reviewStatus": "completed",
"createdAt": "2023-03-14 12:50:27+0000",
"createdAtMs": "2023-03-14 12:50:27.238",
"clientId": "SumsubClient"
}
If you're not receiving webhooks from us try to check your endpoints first with SSL Labs or Docker.
# Verifying webhook sender
It's better to not rely on our IP addresses for whitelisting them as webhook sender because they can change from time to time.
In order to make sure that a webhook is sent by Sumsub, we have the possibility to sign it with the HMAC
algorithm.
If you want to utilize this feature, set a Secret Key value for each webhook in the Dashboard.
We also send the additional X-Payload-Digest-Alg
header which specifies one of the following algorithms to use:
HMAC_SHA1_HEX
(Legacy, deprecated)HMAC_SHA256_HEX
(default upon creating a new webhook)HMAC_SHA512_HEX
Choose the algorithm when configuring webhooks in the Dashboard.
To verify that a webhook is sent by Sumsub:
- Get a webhook
x-payload-digest
header value and payload as it is without any alteration or converting to json. - Receive the HTTP webhook body in bytes.
- Calculate digest with raw webhook payload in bytes and the HMAC algorithm specified in the
x-payload-digest-alg
header. - Compare
x-payload-digest
header value with calculated digest.
In order to check that you compute the digest the same way we do, you can call the following endpoint. This endpoint takes text as a body and cannot process JSON payload as for real cases.
POST /resources/inspectionCallbacks/testDigest?secretKey={secretKey}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
#{body} | String | Yes | Any text |
secretKey | String | Yes | A secret key that might be used for signing |
# RESPONSE
Value | Description |
---|---|
digest | Result of the calculation |
Please test your webhook before sending its URL to us. At a minimum, it should not give a 500 HTTP response or require any sort of authorization.
# Example request to get digest
curl -X POST \
'https://api.sumsub.com/resources/inspectionCallbacks/testDigest?secretKey=SoMe_SeCrEt_KeY' \
-H 'Content-Type: text/plain' \
-d 'someText'
# Example response
{
"digest": "f6e92ffe371718694d46e28436f76589312df8db"
}
# Example request to client's endpoint
curl -X POST \
'https://callbackurl.com/kyc' \
-H 'Content-Type: application/json' \
-d '{
"applicantId": "5cb56e8e0a975a35f333cb83",
"inspectionId": "5cb56e8e0a975a35f333cb84",
"correlationId": "req-ec508a2a-fa33-4dd2-b93d-fcade2967e03",
"externalUserId": "12672",
"type": "applicantReviewed",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"createdAtMs": "2020-02-21 13:23:19.111",
"clientId": "SumsubClient"
}'
# Example of computing digest
export function checkDigest(req): boolean {
const algo = {
'HMAC_SHA1_HEX': 'sha1',
'HMAC_SHA256_HEX': 'sha256',
'HMAC_SHA512_HEX': 'sha512',
}[req.headers['X-Payload-Digest-Alg']]
if (!algo) {
throw new Error('Unsupported algorithm')
}
const calculatedDigest = crypto
.createHmac(algo, SUMSUB_PRIVATE_KEY)
.update(req.rawBody)
.digest('hex')
return calculatedDigest === req.headers['x-payload-digest']
}
private async Task<bool> CheckDigest(HttpRequest request)
{
using (var reader = new StreamReader(request.Body))
{
var body = await reader.ReadToEndAsync();
byte[] byteArray = Encoding.UTF8.GetBytes(body);
MemoryStream stream = new MemoryStream(byteArray);
string algo = Request.Headers["x-payload-digest-alg"];
var calculateDigest = new { };
switch (algo)
{
case "HMAC_SHA1_HEX":
HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(_verificationAccessor.SumSubPrivateKey));
calculateDigest = hmacsha1.ComputeHash(stream).Aggregate("", (s, e) => s + String.Format("{0:x2}", e), s => s);
break;
case "HMAC_SHA256_HEX":
HMACSHA256 hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(_verificationAccessor.SumSubPrivateKey));
calculateDigest = hmacsha1.ComputeHash(stream).Aggregate("", (s, e) => s + String.Format("{0:x2}", e), s => s);
break;
case "HMAC_SHA512_HEX":
HMACSHA512 hmacsha512 = new HMACSHA512(Encoding.UTF8.GetBytes(_verificationAccessor.SumSubPrivateKey));
calculateDigest = hmacsha512.ComputeHash(stream).Aggregate("", (s, e) => s + String.Format("{0:x2}", e), s => s);
break;
default:
HMACSHA256 hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(_verificationAccessor.SumSubPrivateKey));
calculateDigest = hmacsha1.ComputeHash(stream).Aggregate("", (s, e) => s + String.Format("{0:x2}", e), s => s);
break;
}
return calculateDigest == Request.Headers["x-payload-digest"];
};
}
public function validateWebHook(HttpRequest $request, string $content): void
{
$algo = match($request->headers->get('X-Payload-Digest-Alg')) {
'HMAC_SHA1_HEX' => 'sha1',
'HMAC_SHA256_HEX' => 'sha256',
'HMAC_SHA512_HEX' => 'sha512',
default => throw new \RuntimeException('Unsupported algorithm'),
};
$res = $request->headers->get('X-Signature') === hash_hmac(
$algo,
$content,
'your_secret_key'
);
if (!$res) {
$this->logger->error('Webhook sumsub sign ' . $content);
throw new LogicProfileException('Webhook sumsub sign ' . $content);
}
}