# Начало работы
Чтобы приступить к работе с IDnGO:
- Войдите в Дешборд.
- Настройте доступы для пользователей в вашей компании, пригласите свою команду и определите роли и разрешения.
- Создайте уровень верификации.
- Кастомизируйте верификацию под ваш сервис.
- Пройдите аутентификацию.
- Выберите подходящий способ интеграции:
WebSDK → iOS SDK → Android SDK → API-интеграция →
# Аутентификация
Чтобы начать работу с SDK и API от IDnGO, все клиенты должны пройти аутентификацию.
# App токен
В разделе «Панель разработчика» Дешборда нужно сгенерировать appToken и secretKey, далее на их основе генерируется accessToken.
Для каждого окружения (Тестовое или Основное) необходим свой App токен.
Внимание:
Значения appToken и secretKey отображаются только один раз в момент создания токена, позже будет невозможно просмотреть или изменить настройки этого токена.
# Заголовки авторизации
Все API-запросы должны содержать следующие заголовки:
X-App-Token— токен приложения, сгенерированный в панели управления;X-App-Access-Sig— подпись запроса в шестнадцатеричном формате, строчными буквами;X-App-Access-Ts— количество секунд с начала Unix-эпохи (в UTC).
Имена заголовков нечувствительны к регистру и могут различаться в зависимости от реализации как на стороне ваших запросов, так и в наших ответах.
# Подпись запроса
Значение X-App-Access-Sig генерируется алгоритмом HMAC-SHA256 с использованием секретного ключа secretKey (предоставляется при создании appToken) на основе строки, полученной путём объединения следующей информации:
- Временная метка (значение заголовка
X-App-Access-Ts);
Временная метка должна быть в пределах 1 минуты от времени нашего сервера. Убедитесь, что время на ваших серверах правильное.
- Название HTTP-метода в верхнем регистре (например, GET или POST);
- URI запроса без имени хоста, начиная со слэша и включая все параметры запроса, например:
/resources/applicants/123?fields=info; - Тело запроса (если есть) в том виде, в каком оно будет отправлено.
Пример строки, из которой будет сгенерирована подпись:
1607551635POST/resources/accessTokens?userId=cfd20712-24a2-4c7d-9ab0-146f3c142335&levelName=basic-kyc-level&ttlInSecs=600
# POST Генерация токена доступа
При инициализации SDK необходимо использовать токен доступа accessToken. Для его получения нужно отправить запрос:
POST /resources/accessTokens?userId={userId}&levelName={levelName}
Токен доступа для верификации пользователя имеет ограниченный доступ к API, например, он действителен только для одного пользователя и не может быть использован для других.
При инициализации SDK убедитесь, что для заголовков авторизации запроса используется пара appToken и secretKey, созданная в правильном окружении (Тестовое или Основное):
- Для тестирования используйте пару
appTokenиsecretKey, созданную в Тестовом окружении (Sandbox); - Для реальных проверок пару
appTokenиsecretKey, созданную в Основном окружении (Production).
Параметры запроса
| Название | Тип | Обязательно | Описание |
|---|---|---|---|
userId | String | Да | Внешний идентификатор пользователя, который будет привязан к токену. Он соответствует externalUserId анкеты пользователя. |
levelName | String | Да | Название уровня верификации. |
ttlInSecs | Integer | Нет | Срок действия токена в секундах (по умолчанию — 600 секунд). |
externalActionId | String | Нет | Внешний идентификатор действия пользователя (actions), который будет привязан к токену. |
Параметр запроса userId должен быть уникальным и осмысленным. Это может быть внешний идентификатор пользователя в вашей системе или адрес электронной почты. Не генерируйте эти идентификаторы случайным образом, если только вы не проводите тестирование.
Внимание:
Если ваш userId или levelName содержит зарезервированные символы (например, «@», «+»), его следует закодировать в URL, иначе вы можете столкнуться с несоответствием подписи.
Ответ
| Название | Тип | Описание |
|---|---|---|
token | String | Сгенерированный токен доступа для верификации пользователя. |
Пример
curl -X POST \
'https://api.cyberity.ru/resources/accessTokens?userId=JamesBond007&levelName=basic-kyc-level&ttlInSecs=600' \
-H 'Accept: application/json'
{
"token": "_act-b8ebfb63-5f24-4b89-9c08-5bbabeec986e",
"userId": "JamesBond007"
}
# Вебхуки
Вебхуки от IDnGO — это инструмент, который позволяет автоматически получать уведомления о различных событиях и изменениях, связанных с процессом верификации ваших пользователей.
После завершения проверки пользователя мы отправим вам POST-запрос с полезной нагрузкой в формате JSON на URL, предоставленный нам при интеграции. Зачастую URL-адреса различаются для Тестового и Основного окружения.
Внимание:
- Мы не отправляем никакой информации на эндпоинт по протоколу HTTP, только HTTPS.
- Поддерживаемые версии протокола TLS — 1.2 или выше.
- Существует ограничение на количество вебхуков: не более 20.
- Мы не передаем никаких личных данных через вебхуки. Вы можете получить все распознанные данные пользователя при помощи этого API-метода.
- Если вы не получаете от нас вебхуков, попробуйте сначала проверить свои эндпоинты с помощью SSL Labs или Docker.
Пожалуйста, протестируйте ваш вебхук, прежде чем отправлять его URL нам. Он не должен выдавать HTTP-ответ 500 или требовать какой-либо авторизации.
Вы можете отслеживать и анализировать каждое событие во вкладке «Журналы вебхуков».
Так как при API-интеграциии в Тестовом окружении (Sandbox) не происходят автоматической проверки, необходимо задать параметры верификации с помощью этого запроса. После чего можно получить результаты проверки в Тестовом окружении через вебхук applicantReviewed.
На случай, если по какой-то причине вебхук не был получен, мы записываем все, что пытаемся отправить вам, и можем повторно отправить вебхук в любой момент. Если вебхук не удается отправить, мы делаем это повторно четыре раза: через 5 минут, 1 час, 5 и 18 часов, до тех пор пока запрос не будет успешным. Рекомендуем вам дожидаться вебхука не более суток, а затем отправить запрос на наш сервер для получения информации о статусе пользователя.
Проверяйте поле createdAtMs полезной нагрузки вебхука, чтобы убедиться, что вы получаете актуальный статус пользователя.
# Типы вебхуков
Настраивать типы, отслеживать статусы вебхуков и отправлять их вручную можно в разделе «Менеджер вебхуков».
| Значение | Описание |
|---|---|
applicantCreated | Создана анкета пользователя. |
applicantPending | Все необходимые документы пользователем загружены и анкета ожидает проверки. |
applicantReviewed | Верификация пользователя завершена. Содержит результат верификации. |
applicantOnHold | Пользователь ожидает окончательного решения от специалиста по соблюдению нормативных требований (начата ручная проверка). |
applicantReset | Анкета пользователя была сброшена: статус пользователя изменился на init, и все документы стали неактивны, ожидается загрузка новых изображений/данных. |
applicantPersonalInfoChanged | Предоставленная информация пользователя была изменена. |
applicantPrechecked | Завершена первичная проверка пользователя. |
applicantDeleted | Пользователь был удален навсегда. |
applicantLevelChanged | Уровень проверки был изменен. |
applicantActionPending | Действия пользователя ожидают проверки. |
applicantActionReviewed | Проверка действий пользователя завершена. |
applicantActionOnHold | Действие пользователя ожидает окончательного решения от специалиста по соблюдению нормативных требований. |
Поля полезной нагрузки вебхука
| Название | Тип | Обязательно | Описание |
|---|---|---|---|
applicantId | String | Да | Идентификатор пользователя. |
inspectionId | String | Да | Идентификатор проверки. |
correlationId | String | Да | Идентификатор, который однозначно идентифицирует событие на нашей стороне. |
levelName | String | Нет | Название уровня верификации. |
externalUserId | String | Нет | Внешний идентификатор пользователя – уникальный идентификатор пользователя на вашей стороне. |
type | String | Да | Тип вебхука. |
sandboxMode | Boolean | Да | True, если вебхук был отправлен из Тестового окружения (Sandbox). |
reviewStatus | String | Да | Текущий статус пользователя. Более подробную информацию можно найти здесь. |
createdAtMs | Date | Да | Дата и время создания вебхука в UTC (формат YYYY-MM-dd hh:mm:ss.fff). |
applicantType | String | Нет | Тип пользователя, например, individual/company. |
reviewResult | Object | Нет | Поле, содержащее дополнительную информацию о результатах верификации пользователя. |
applicantMemberOf | Array of objects | Нет | Содержит список анкет компаний, к которым относится текущий пользователь в качестве бенефициара. |
applicantActionId | String | Нет | Идентификатор действия пользователя. |
externalApplicantActionId | String | Нет | Уникальный идентификатор действия на вашей стороне. |
clientId | String | Да | Уникальный идентификатор вас как нашего клиента. |
# Причины отказа
Вебхук с результатом верификации содержит поле rejectLabels, в котором указаны один или более тегов, описывающих причину отказа. Теги rejectLabels нужны только для анализа результатов проверок и они лишь обобщенно описывают проблему, поэтому не стоит использовать их для генерации комментариев для пользователя.
| Тег отказа (значение rejectLabels) | Тип отказаreviewRejectType | Описание |
|---|---|---|
FORGERY | FINAL | Попытка мошенничества. |
SPAM | FINAL | Было загружено слишком много изображений (спам фотографиями). |
BAD_PROOF_OF_IDENTITY | RETRY | Тип предоставленного документа не подходит для верификации/на изображении нет документа, документ без подписей или печатей. |
SELFIE_MISMATCH | FINAL | Фотография пользователя (селфи) не совпадает с фотографией на предоставленных документах. |
ID_INVALID | RETRY | Документ, удостоверяющий личность, недействителен. |
DUPLICATE | FINAL | У этого пользователя уже есть проверенная ранее анкета, а дублирование не допускается настройками. |
BAD_AVATAR | RETRY | Аватар не соответствует требованиям. |
WRONG_USER_REGION | FINAL | Запрещены пользователи из данного региона/страны. |
INCOMPLETE_DOCUMENT | RETRY | Документ на фотографии виден частично из-за чего отсутствует часть информации. |
BLACKLIST | FINAL | Пользователь занесен в черный список нами. |
BLOCKLIST | FINAL | Пользователь занесен в черный список вами. |
UNSATISFACTORY_PHOTOS | RETRY | На изображении видны следы обработки в графическом редакторе. |
DOCUMENT_PAGE_MISSING | RETRY | Отсутствуют нужные для проверки страницы документа. |
DOCUMENT_DAMAGED | RETRY | Документ поврежден. |
REGULATIONS_VIOLATIONS | FINAL | Пользователь не соответствует требованиям проверки. |
INCONSISTENT_PROFILE | FINAL | В анкете были обнаружены данные или документы разных людей. |
ADDITIONAL_DOCUMENT_REQUIRED | RETRY | Для прохождения верификации требуются дополнительные документы. |
AGE_REQUIREMENT_MISMATCH | FINAL | Возраст пользователя не соответствует требованиям. |
EXPERIENCE_REQUIREMENT_MISMATCH | FINAL | Опыт пользователя не соответствует требованиям (например, опыт вождения). |
CRIMINAL | FINAL | Пользователь был вовлечен в незаконную деятельность. |
WRONG_ADDRESS | RETRY | Адрес из документов не совпадает с адресом, который ввел пользователь. |
GRAPHIC_EDITOR | RETRY | Фотография была отредактирована в графическом редакторе. |
DOCUMENT_DEPRIVED | RETRY | Документ у пользователя был изъят. |
FRAUDULENT_PATTERNS | FINAL | Обнаружено мошенническое поведение. |
NOT_ALL_CHECKS_COMPLETED | RETRY | Не все проверки были завершены. |
FRONT_SIDE_MISSING | RETRY | Отсутствует лицевая сторона/главный разворот документа. |
BACK_SIDE_MISSING | RETRY | Обратная сторона/разворот документа отсутствует. |
SCREENSHOTS | RETRY | Пользователь загрузил скриншоты. |
BLACK_AND_WHITE | RETRY | Пользователь загрузил черно-белые фотографии документов. |
INCOMPATIBLE_LANGUAGE | RETRY | Требуется перевод документа. |
EXPIRATION_DATE | RETRY | Пользователь загрузил документ с истекшим сроком действия. |
BAD_SELFIE | RETRY | Пользователь загрузил плохое селфи. |
BAD_FACE_MATCHING | RETRY | Не удается сверить лицо в документе с лицом на селфи. |
BAD_PROOF_OF_ADDRESS | RETRY | Пользователь загрузил плохой документ подтверждащий адрес проживания PoA. |
FRAUDULENT_LIVENESS | FINAL | Была попытка обойти проверку живости (liveness). |
OTHER | RETRY | Иная причина. |
PROBLEMATIC_APPLICANT_DATA | RETRY | Предоставленная информация не совпадает с информацией, полученной из документа. |
OK | RETRY | Пользовательский тег отказа. |
Пример
# Пример полезной нагрузки вебхука applicantReviewed:
{
"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: support@cyberity.ru",
// 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.129"
}
{
"applicantId": "5cb56e8e0a975a35f333cb83",
"inspectionId": "5cb56e8e0a975a35f333cb84",
"correlationId": "req-a260b669-4f14-4bb5-a4c5-ac0218acb9a4",
"externalUserId": "externalUserId",
"type": "applicantReviewed",
"reviewResult": {
"reviewAnswer": "GREEN"
},
"reviewStatus": "completed",
"createdAtMs": "2020-02-21 13:23:19.091"
}
# Пример полезной нагрузки вебхука applicantCreated:
{
"applicantId": "5c9e177b0a975a6eeccf5960",
// inspection ID that contains a result
"inspectionId": "5c9e177b0a975a6eeccf5961",
// an ID to debug in case of unexpected errors (should be provided to IDnGO)
"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.001",
"clientId": "cyberityClient"
}
# Пример полезной нагрузки вебхука applicantPending:
{
"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.001",
"clientId": "cyberityClient"
}
# Пример полезной нагрузки вебхука applicantOnHold:
{
"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.001",
"clientId": "cyberityClient"
}
# Пример полезной нагрузки вебхука applicantPersonalInfoChanged:
{
"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.001",
"clientId": "cyberityClient"
}
# Пример полезной нагрузки вебхука applicantPrechecked:
{
"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.001",
"clientId": "cyberityClient"
}
# Пример полезной нагрузки вебхука applicantDeleted:
{
"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.001",
"clientId": "cyberityClient"
}
# Пример полезной нагрузки вебхука applicantLevelChanged:
{
"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": "cyberityClient"
}
# Пример полезной нагрузки вебхука applicantReset:
{
"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.001",
"clientId": "cyberityClient"
}
# Проверка отправителя вебхука
Лучше не полагаться на наши IP-адреса при составлении белого списка отправителей вебхуков, поскольку они могут время от времени меняться.
Чтобы убедиться, что вебхук отправлен именно нами, можно воспользоваться подписью с помощью алгоритма HMAC. Если вы хотите воспользоваться этой функцией, задайте значение секретного ключа для каждого вебхука и выберите алгоритм HMAC в «Панели разработчика» Дешборда.
При использовании подписи мы также отправляем дополнительный заголовок X-Payload-Digest-Alg, в котором указывается один из следующих алгоритмов:
HMAC_SHA1_HEX— устарелHMAC_SHA256_HEX— используется по умолчанию при создании нового вебхукаHMAC_SHA512_HEX
Инструкция проверки отправителя вебхука:
- Получите значение заголовка вебхука
x-payload-digestи полезную нагрузку как она есть, без каких-либо изменений или преобразования в JSON. - Получите тело HTTP-вебхука в байтах.
- Вычислите контрольное значение (
digest) с использованием исходной полезной нагрузки в байтах и алгоритма HMAC, указанного в заголовкеx-payload-digest-alg. - Сравните значение заголовка
x-payload-digestс вычисленным вами контрольным значением.
Проверка отправителя вебхука: POST /resources/inspectionCallbacks/testDigest?secretKey={secretKey}
# Пример запроса к вашему эндпоинту:
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": "CyberityClient"
}'
# Пример вычисления контрольного значения 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, CYBERIRTY_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.CyberityPrivateKey));
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.CyberityPrivateKey));
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.CyberityPrivateKey));
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.CyberityPrivateKey));
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 Cyberity sign ' . $content);
throw new LogicProfileException('Webhook Cyberity sign ' . $content);
}
}