# 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.
We have two environments:
- Test environment
test-api.sumsub.com
is only needed to implement integration with Sum&Substance. No checks are performed on the test environment. - In order to provide you with access to the production environment
api.sumsub.com
we require that the integration is tested first. We just want to make sure that everything goes right.
# 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 in our dashboard for test and production environments.
# 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&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
When initializing WebSDK or MobileSDK, an access token authentication must be used.
POST /resources/accessTokens
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
ttlInSecs | Integer | No | Lifespan of a token in seconds. Default value is equal to 10 mins. |
userId | String | Yes | An external user ID which will be bound to the token. It correlate to externalUserId of an applicant. |
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
contains reserved characters (e.g. "@", "+") it should be URL encoded otherwise you may get signature mismatch.
# 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://test-api.sumsub.com/resources/accessTokens?userId=JamesBond007&ttlInSecs=600' \
-H 'Accept: application/json'
# Example response
{
"token": "_act-b8ebfb63-5f24-4b89-9c08-5bbabeec986e",
"userId": "JamesBond007"
}
# Errors
On API errors we return standard HTTP Status Codes.
Please note that we have a rate limiter 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 | Always present | Description |
---|---|---|---|
description | String | Yes | Human-readable error description. |
code | Integer | Yes | HTTP Status code. |
correlationId | String | Yes | 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 | No | Code of the exact problem (see the Error codes section). For some errors may not be present. |
errorName | String | No | 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. |
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. |
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.
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
levelName | String | Yes | Name of user verification steps configuration that you should set up at the dashboard on test and production environments. |
#{body} | Object | Yes | An object representing an applicant (see example). |
Please note that the levelName
is case sensitive and have to be created at the same environment (test-api.sumsub.com
or api.sumsub.com
) that you're calling via API.
# 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. |
String | No | Applicant email. | |
lang | String | No | The language in which the applicant should see the result of verification. |
metadata | Array of objects | No | Additional information that is not displayed to the end user ( Example: [{"key": "keyFromClient", "value": "valueFromClient"}] ). |
info | Object | No | Basic information about the applicant. |
fixedInfo | Object | No | Basic information about the applicant that we shouldn't change from our side. Has the same attributes as info . |
# info
ATTRIBUTE
Name | Type | Required | Description |
---|---|---|---|
firstName | String | No | First name. |
lastName | String | No | Last name. |
middleName | String | No | Middle name. |
legalName | String | No | Legal name. |
gender | String | No | Sex of a person. |
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 RUS ) (Wikipedia). |
nationality | String | No | Alpha-3 country code (Wikipedia). |
phone | String | No | Phone number. |
addresses | Array | No | List of addresses. |
# 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. |
startDate | String | No | Start date of stay in current building (format YYYY-mm-dd , e.g. 2001-09-25). |
endDate | String | No | End date of stay in current building (format YYYY-mm-dd , e.g. 2002-10-26). |
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
# Example request
curl -X POST \
'https://test-api.sumsub.com/resources/applicants?levelName=basic-kyc-level' \
-H 'Content-Type: application/json' \
-d '{
"externalUserId": "someUniqueUserId",
"email": "[email protected].com",
"info": {
"country": "GBR",
"firstName": "John",
"lastName": "Smith",
"phone": "+449112081223",
"dob": "2000-03-04",
"placeOfBirth": "London"
}
}'
# Example response
{
"id": "5f80e6b7155a6336271e4677", //applicantId
"createdAt": "2020-10-09 22:39:51",
"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": {
"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)
This method updates required documents list according to the level provided. In case you need to add one more step to the check, for example, only the id document and the selfie have been requested at first, and after the check has been completed you need to get the proof of address, so you need to add one more step to the current list of required documents.
If you're using our SDKs you can just initialize it with suitable flowName
and SDK will change level automatically.
POST /resources/applicants/{applicantId}/moveToLevel?name={levelName}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
levelName | String | Yes | Name of new user verification steps configuration from the dashboard on test and production environments. |
# RESPONSE
A new requiredIdDocs
and levelName
will be returned.
# Example request with body for changing required document set
curl -X POST \
'https://test-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://test-api.sumsub.com/resources/applicants/5f80e6b7155a6336271e4677/requiredIdDocs' \
-H 'Content-Type: application/json'
# Adding an ID document
A method gets a multipart form: ID doc JSON metadata and, optionally, a document photo. If the ID doc with this type already exists, its data will be merged. Existing data will be overwritten if they also present in the new object. However, a new image will be added nonetheless. If document metadata are not known yet, just send type and a country. E.g. "PASSPORT" and "GBR". These two fields are mandatory.
POST /resources/applicants/{applicantId}/info/idDoc
# REQUEST HEADERS
Name | Type | Value |
---|---|---|
Content-Type | String | multipart/form-data |
# 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 |
# 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
# Example request
curl -X POST \
'https://test-api.sumsub.com/resources/applicants/5b75a5e80a975a3ef2102a87/info/idDoc' \
-H 'content-type: multipart/form-data' \
-F 'metadata={"idDocType":"PASSPORT","country":"USA"}' \
-F 'content={path to file (file contents)}'
# Example response
{
"idDocType": "PASSPORT",
"country": "USA"
}
In cases when only the data of the document needs to upload:
curl -X POST \
'https://test-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 |
BANK_CARD | A bank card, like Visa or Maestro |
UTILITY_BILL | A utility bill |
BANK_STATEMENT | A bank statement |
SELFIE | A selfie with a document (in this case no additional metadata should be sent) |
VIDEO_SELFIE | A selfie video (can be used in webSDK or mobileSDK) |
PROFILE_IMAGE | A profile image, i.e. userpic (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 |
RESIDENCE_PERMIT | Residence permit or registration document in the foreign city/country |
EMPLOYMENT_CERTIFICATE | A document from an employer, e.g. proof that a user works there |
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) |
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 in cases where a verification has been completed.
The info.idDocs
field contains information extracted from applicants’ documents.
As the described endpoint is actually quite generic (e.g. may return several applicants by external id,
but we omit such details in this document),
it returns a list of items. However, if you provide an applicant id that exists, only 1 item will be returned.
# Example request
curl -X GET \
'https://test-api.sumsub.com/resources/applicants/5b594ade0a975a36c9349e66/one'
# Example response
{
"id": "5b594ade0a975a36c9349e66",
"createdAt": "2020-06-24 05:05:14",
"clientId": "ClientName",
"inspectionId": "5b594ade0a975a36c9379e67",
"externalUserId": "SomeExternalUserId",
"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",
"issuedDate": "2018-09-05",
"validUntil": "2028-09-04",
"number": "LGXX359T8",
"dob": "1989-07-16",
"mrzLine1": "IDD<<LGXX359T88<<<<<<<<<<<<<<<",
"mrzLine2": "8907167<2809045D<<<<<<<<<<<<<8",
"mrzLine3": "SMITH<<CHRISTIAN<<<<<<<<<<<<<<"
}
]
},
"agreement": { //present when WebSDK 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",
"startDate": "2020-06-24 05:11:22+0000",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"notificationFailureCnt": 0
},
"lang": "de",
"type": "individual"
}
Here you can find examples of recognition results for all supported document types.
# Changing applicant data
If you want to change an applicant's data you can issue a PATCH
request instead of creating a new applicant, which is highly discouraged. This method patches the fields in the info
key of the applicant.
PATCH /resources/applicants/{applicantId}/info
# REQUEST ARGUMENTS
The body must contain only those fields that you intend to change. Null fields will be ignored. If you want to unset some fields, provide these in the unsetFields
query parameter. Check the info
attribute above for the list of supported fields.
Name | Type | Required | Description |
---|---|---|---|
#{body} | Object | No | Field in the info attribute that should be changed |
applicantId | String | Yes | Applicant ID |
unsetFields | String | No | Comma-separated list of fields to unset |
# RESPONSE
A patched info
entity. As this endpoint could be quite damaging if misused, please check that it returns expected results on the test environment — it's a client's responsibility to treat their applicants well 😃
# Example request
curl -X PATCH \
'https://test-api.sumsub.com/resources/applicants/5b76df770a975a1404cbcb60/info' \
-H 'Content-Type: application/json' \
-d '{
"firstName" : "Andrew",
"dob" : "1990-05-01"
}'
# Example response
{
"firstName": "Andrew",
"dob": "1990-05-01",
"country": "GBR",
"idDocs": [
{
"idDocType": "PASSPORT",
"country": "GBR",
"number": ""
},
{
"idDocType": "DRIVERS",
"country": "GBR"
}
]
}
# 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. Oftentimes, 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 on 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 createdAt
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 dashboard on test and production.
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 blacklisted |
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) |
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 |
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 | A reject 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",
"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
"createdAt": "2020-02-21 13:23:19+0000"
}
# Example webhook payload with 'GREEN' answer
{
"applicantId": "5cb56e8e0a975a35f333cb83",
"inspectionId": "5cb56e8e0a975a35f333cb84",
"correlationId": "req-a260b669-4f14-4bb5-a4c5-ac0218acb9a4",
"externalUserId": "externalUserId",
"type": "applicantReviewed",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"createdAt": "2020-02-21 13:23:19+0000"
}
# 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 status (SDK)
It is recommended that you use this method if you are using a WebSDK.
GET /resources/applicants/{applicantId}/status
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
# RESPONSE
Name | Type | Optional | Value |
---|---|---|---|
createDate | Date | No | Date of creation of the applicant. |
reviewDate | Date | No | Date of check ended. |
startDate | Date | No | Date of check started. |
moderationComment | String | Yes | A human-readable comment that can be shown to the end user. |
clientComment | String | Yes | A human-readable comment that should not be shown to the end user. |
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. |
queued | The checks have been started for the applicant. |
completed | The check has been completed. |
onHold | Requires investigation - applicant waits for a final decision. |
# Example request
curl -X GET \
'https://test-api.sumsub.com/resources/applicants/5b76df770a975a1404cbcb60/status'
# Example response
{
"id": "5b76df770a975a1404cbcb60",
"inspectionId": "5b76df770a975a1404cbcb61",
"applicantId": "5b76df770a975a1404cbcb60",
"createDate": "2018-10-06 12:47:57+0000",
"startDate": "2018-08-17 14:51:59+0000",
"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"
},
"reviewStatus": "completed",
"notificationFailureCnt": 0
}
# Getting applicant status (API)
It is recommended that you use this method if you not only need an applicant’s status, but also information about documents.
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). You can get images with those IDs by issuing a request described here.
# Example request:
curl -X GET \
'https://test-api.sumsub.com/resources/applicants/5bb8cca10a975a624903cf65/requiredIdDocsStatus'
# 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
"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
"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"
}
}
}
}
# Requesting an applicant re-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 want to request some additional checks agreed with us in advance. To do so you should
explicitly move an 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 | Status change reason |
Provide a reason if available. It will help us understand why you want to recheck an applicant. If agreed in advance the reason you will be sending, we can automate the process and e.g. perform some additional checks and notify you about the results by the usual webhook means. If you want to automate complaints to your support, just send a user message along and we will consider it on a case-by-case basis.
# RESPONSE
In case of HTTP response 200
, you may ignore the response body.
# Example request
curl -X POST \
'https://test-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 programmatically request applicant check with flow that somehow 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://test-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 more 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://test-api.sumsub.com/resources/inspections/5bb8cca10a975a624903cf66/resources/551262049'
# Adding an applicant to blacklist
If for some reason you need to add an applicant to the blacklist, you can use this endpoint. It is necessary to add the reason for adding the applicant to the blacklist.
POST /resources/applicants/{applicantId}/blacklist
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
applicantId | String | Yes | Applicant ID |
note | String | Yes | Reason or note for the applicant added to the blacklist (URL encoded) |
# RESPONSE
The answer will contain the applicant added to the blacklist.
# Example request
curl -X POST \
'https://test-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": {
"country": "GBR",
"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": ["BLACKLIST"],
"reviewRejectType": "FINAL"
},
"reviewStatus": "completed",
"notificationFailureCnt": 0
}
}
# 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
# 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://test-api.sumsub.com/resources/accessTokens/-/shareToken?applicantId=5ce412012b4da877b2d910bd&forClientId=CoolCoinLtd'
# Example response
{
"token": "_act-460a698b-d2bc-4cbc-9456-5f36fee38083",
"forClientId": "CoolCoinLtd"
}
# 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 (test and production) | |
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://test-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 | |
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 |
# RESPONSE
Applicant entity in service Y
. Note a new applicant ID returned in the response. Bind it in your system as needed.
Please note that share tokens will be invalidated once used.
# 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. Or if the applicant has been rejected with FINAL
reviewRejectType and needs to be allowed to re-upload the documents.
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://test-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 | |
sourceKey | String | No | New sourceKey |
type | String | No | New type (individual /company ) |
lang | String | No | The language in which the applicant should see result of verification |
metadata | Array of objects | No | Additional information about user at custom fields |
# Example request
curl -X PATCH \
'https://test-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"
}
# Import applicants
This method is intended for data import. For example, in case if 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
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
#{body} | Object | Yes | An object representing an applicant (see example). |
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://test-api.sumsub.com/resources/applicants/-/ingestCompleted' \
-H 'Content-Type: application/json' \
-d '{
"externalUserId": "someClientUserId",
"info": {
"firstName": "John",
"lastName": "Snow",
"country": "GBR"
},
"review": {
"elapsedSincePendingMs": 100,
"elapsedSinceQueuedMs": 50,
"createDate": "2020-10-21 08:25:32+0000",
"reviewDate": "2020-10-21 08:25:36+0000",
"startDate": "2020-10-21 08:25:35+0000",
"reviewResult": {
"moderationComment": "",
"clientComment": "",
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed"
},
"requiredIdDocs": {
"docSets": [{
"idDocSetType": "IDENTITY",
"types": [
"PASSPORT",
"ID_CARD",
"DRIVERS"
]
}
]
}
}'
# 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": "IDENTITY",
"types": [
"PASSPORT",
"ID_CARD",
"DRIVERS"
]
}]
},
"review": {
"elapsedSincePendingMs": 100,
"elapsedSinceQueuedMs": 50,
"createDate": "2019-10-21 08:25:32+0000",
"reviewDate": "2019-10-21 08:25:36+0000",
"startDate": "2019-10-21 08:25:35+0000",
"reviewResult": {
"moderationComment": "",
"clientComment": "",
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed"
},
"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 you 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://test-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 that method to assign custom tags to applicant profiles. Create new tags in the Global settings section of the dashboard on test and production
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://test-api.sumsub.com/resources/applicants/5fd1012c885b5d0009d50e6e/tags' \
-H 'Content-Type: application/json' \
-d '["Approved Sanctions", "Approved PEP", "821"]'
# Example response
{
"ok": 1
}
# Webhooks
A webhook is an HTTP POST request sent to a target you define at Dev space dashboard on test and production environments.
The test server 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.
We can send webhooks not only via HTTP request, but also as notifications in Slack, Telegram or via email.
# Webhooks 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. |
videoIdentStatusChanged | Status of Video Ident type of verification has been changed. |
applicantActionReviewed | Applicant action verification has been completed. More info about applicant actions you can find here. |
applicantActionOnHold | Applicant action verification has been paused for an agreed reason. More info about applicant actions you can find here. |
Example applicantReviewed webhook payload you can find here.
# 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",
"externalUserId": "12672",
// type of webhook (see the corresponding section)
"type": "applicantCreated",
"reviewStatus": "init",
"createdAt": "2020-02-21 13:23:19+0000"
}
# Example applicantPending webhook payload:
{
"applicantId": "5c7791f80a975a1df426b9e9",
"inspectionId": "5c7791f80a975a1df426b9ea",
"applicantType" : "individual",
"correlationId": "req-4af54c06-6a50-4cb9-a7dc-b94b2f5b07eb",
"externalUserId": "12672",
"type": "applicantPending",
"reviewStatus": "pending",
"createdAt": "2020-02-21 13:23:19+0000"
}
# Example applicantOnHold webhook payload:
{
"inspectionId": "5d10ca4e0a975a1c4cc30bbb",
"applicantType" : "individual",
"correlationId": "req-a98abc30-a5d9-4e1d-bab4-2f1af64bd5a5",
"externalUserId": "12672",
"reviewStatus": "onHold",
"applicantId": "5d10ca4e0a975a1c4cc30bba",
"type": "applicantOnHold",
"createdAt": "2020-02-21 13:23:19+0000"
}
# Example applicantPersonalInfoChanged webhook payload:
{
"applicantId" : "5ede51230a975a19a19ba5c1",
"inspectionId" : "5ede51230a975a19a19ba5c2",
"applicantType" : "individual",
"correlationId" : "req-60103dee-79f1-43f4-bdcc-eb2554556afa",
"externalUserId" : "12672",
"type" : "applicantPersonalInfoChanged",
"reviewResult" : {
"reviewAnswer" : "GREEN"
},
"reviewStatus" : "completed",
"createdAt" : "2020-06-08 19:39:29+0000"
}
# Example applicantPrechecked webhook payload:
{
"applicantId": "5d1f2914c2d75a1c14130bd2",
"inspectionId": "5d1f2914c2d75a1c14130bd3",
"applicantType" : "individual",
"correlationId": "req-e9d77142-59e6-4713-9b07-9b342cc51dda",
"externalUserId": "12672",
"type": "applicantPrechecked",
"reviewStatus": "queued",
"createdAt": "2020-02-21 13:23:19+0000"
}
# Example applicantDeleted webhook payload:
{
"applicantId": "5f194e74040c3f316bda271c",
"inspectionId": "5f194e74040c3f316bda271d",
"applicantType": "individual",
"correlationId": "req-d34c974c-5935-41b8-a0a9-cedd2407eada",
"externalUserId": "12672",
"type": "applicantDeleted",
"reviewStatus": "init",
"createdAt": "2020-07-23 11:18:33+0000"
}
# Example videoIdentStatusChanged webhook payload when video call has been initialized:
{
"applicantId": "5f8993f93324610009e5885e",
"inspectionId": "5f8993f93324610009e5885f",
"applicantType": "individual",
"correlationId": "req-5a570d5d-e27c-484b-94a9-0f1e9fb2764c",
"externalUserId": "flow-feb1f7ad-b2e2-479a-a4d5-bca5486f7b85",
"type": "videoIdentStatusChanged",
"reviewStatus": "init",
"videoIdentReviewStatus": "pending",
"createdAt": "2020-10-16 12:37:26+0000"
}
# Example videoIdentStatusChanged webhook payload when video call has been completed:
{
"applicantId": "596928690a975a137c5b207e",
"inspectionId": "596928690a975a137c5b207a",
"correlationId": "req-a7812631-2c40-4f13-8407-fd42a516f0b0",
"type": "videoIdentStatusChanged",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"videoIdentReviewStatus": "pending",
"createdAt": "2020-10-16 12:34:25+0000"
}
# Testing on the test environment
On a test environment 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 | String | Yes | Reasons for rejections |
reviewRejectType | String | No | Choose EXTERNAL , 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://test-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://test-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
}
# Verifying webhook sender
It's better to not rely on our IP addresses for whitelisting them as webhook sender because they're changing from time to time.
In order to make sure that a webhook is sent by us, we have the possibility to sign it with the HMAC Sha1Hex
algorithm. If you want to utilize this feature, we need to agree on a secret key that will be used for signing the HTTP webhook body. To compute digest
correctly, you need to convert the HTTP webhook body into bytes and then perform calculations. The header x-payload-digest
will be added to the webhook. In order to verify that a webhook is sent by us, you compute the same digest using the HTTP body and a secret key and compare it to the digest
value that we've sent to you.
In order to check that you compute the digest the same way we do, you can call the following endpoint.
POST /resources/inspectionCallbacks/testDigest?secretKey={secretKey}
# REQUEST ARGUMENTS
Name | Type | Required | Description |
---|---|---|---|
#{body} | Object | Yes | Any payload |
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://test-api.sumsub.com/resources/inspectionCallbacks/testDigest?secretKey=SoMe_SeCrEt_KeY' \
-H 'Content-Type: text/plain' \
-d 'someText'
# Example response
{
"digest": "34fbf271ecf2b6a0712025de9f83c484405283e2"
}
# 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",
"createdAt": "2020-02-21 13:23:19+0000"
}'
# Example of computing digest
export function checkDigest(req): boolean {
const calculatedDigest = crypto
.createHmac('sha1', 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();
HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.ASCII.GetBytes(_verificationAccessor.SumSubPrivateKey));
byte[] byteArray = Encoding.ASCII.GetBytes(body);
MemoryStream stream = new MemoryStream(byteArray);
var calculateDigest = hmacsha1.ComputeHash(stream).Aggregate("", (s, e) => s + String.Format("{0:x2}", e), s => s);
return calculateDigest == Request.Headers["x-payload-digest"];
};
}