curl --request POST \
--url http://localhost:8080/api/v1/kyc/applications \
--header 'Content-Type: application/json' \
--data '
{
"allowed_documents": [
"<string>"
],
"applicant_email": "<string>",
"applicant_phone": "<string>",
"client_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>",
"locale": "<string>",
"max_attempts": 123,
"metadata": "<unknown>",
"profile": "<unknown>",
"redirect_url": "<string>",
"require_video": true,
"ttl_hours": 123
}
'import requests
url = "http://localhost:8080/api/v1/kyc/applications"
payload = {
"allowed_documents": ["<string>"],
"applicant_email": "<string>",
"applicant_phone": "<string>",
"client_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>",
"locale": "<string>",
"max_attempts": 123,
"metadata": "<unknown>",
"profile": "<unknown>",
"redirect_url": "<string>",
"require_video": True,
"ttl_hours": 123
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
allowed_documents: ['<string>'],
applicant_email: '<string>',
applicant_phone: '<string>',
client_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
external_id: '<string>',
locale: '<string>',
max_attempts: 123,
metadata: '<unknown>',
profile: '<unknown>',
redirect_url: '<string>',
require_video: true,
ttl_hours: 123
})
};
fetch('http://localhost:8080/api/v1/kyc/applications', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "8080",
CURLOPT_URL => "http://localhost:8080/api/v1/kyc/applications",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'allowed_documents' => [
'<string>'
],
'applicant_email' => '<string>',
'applicant_phone' => '<string>',
'client_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'external_id' => '<string>',
'locale' => '<string>',
'max_attempts' => 123,
'metadata' => '<unknown>',
'profile' => '<unknown>',
'redirect_url' => '<string>',
'require_video' => true,
'ttl_hours' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://localhost:8080/api/v1/kyc/applications"
payload := strings.NewReader("{\n \"allowed_documents\": [\n \"<string>\"\n ],\n \"applicant_email\": \"<string>\",\n \"applicant_phone\": \"<string>\",\n \"client_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"external_id\": \"<string>\",\n \"locale\": \"<string>\",\n \"max_attempts\": 123,\n \"metadata\": \"<unknown>\",\n \"profile\": \"<unknown>\",\n \"redirect_url\": \"<string>\",\n \"require_video\": true,\n \"ttl_hours\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://localhost:8080/api/v1/kyc/applications")
.header("Content-Type", "application/json")
.body("{\n \"allowed_documents\": [\n \"<string>\"\n ],\n \"applicant_email\": \"<string>\",\n \"applicant_phone\": \"<string>\",\n \"client_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"external_id\": \"<string>\",\n \"locale\": \"<string>\",\n \"max_attempts\": 123,\n \"metadata\": \"<unknown>\",\n \"profile\": \"<unknown>\",\n \"redirect_url\": \"<string>\",\n \"require_video\": true,\n \"ttl_hours\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://localhost:8080/api/v1/kyc/applications")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"allowed_documents\": [\n \"<string>\"\n ],\n \"applicant_email\": \"<string>\",\n \"applicant_phone\": \"<string>\",\n \"client_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"external_id\": \"<string>\",\n \"locale\": \"<string>\",\n \"max_attempts\": 123,\n \"metadata\": \"<unknown>\",\n \"profile\": \"<unknown>\",\n \"redirect_url\": \"<string>\",\n \"require_video\": true,\n \"ttl_hours\": 123\n}"
response = http.request(request)
puts response.read_body{
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"link_expires_at": "2023-11-07T05:31:56Z",
"link_url": "<string>",
"status": "CREATED",
"client_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>"
}Create a KYC Application (verification link)
Create a KYC application and get back a verification link to give your customer.
This is asynchronous. The call returns as soon as a link exists — long before your customer has done anything with it. The verdict arrives later, at the webhook endpoints registered for your business. There is no synchronous variant: a verification takes as long as the applicant takes, which is unbounded.
The flow
POST /api/v1/kyc/applications→ you getlink_urlandlink_expires_at.- Send
link_urlto your customer (SMS, email, in-app — your channel). - They photograph their ID and record a short video on their own phone. The images go straight to the verification service; they never pass through Dinari, and Dinari never stores your customer’s name or document number.
- You receive
KYC_COMPLETED,KYC_REJECTED,KYC_NEEDS_REVIEW,KYC_FAILEDorKYC_EXPIREDat your webhook endpoint.
Reading the outcome
KYC_COMPLETED— verified. Safe to onboard.KYC_REJECTED— the check failed.reason_codessays why.KYC_NEEDS_REVIEW— not a soft approval. A compliance officer must decide, and a possible sanctions match is never auto-cleared. Do not treat this as a yes. You will receiveKYC_REVIEW_RESOLVEDwhen it is decided.KYC_FAILED— the pipeline failed, not the applicant. They can retry on the same link if attempts remain.KYC_EXPIRED— the link lapsed unused. Create a new application.
Notes
external_idis your own identifier for the applicant. It is echoed on every webhook, so you never have to store our ids.metadatais an opaque object echoed back untouched.require_videodefaults to true and should stay that way. A single still photograph cannot establish that a live person is present, so a still-only application can never be auto-approved and will always land in review.- Links are single-purpose and expire. Treat
link_urlas a secret: anyone holding it can submit documents against this application.
curl --request POST \
--url http://localhost:8080/api/v1/kyc/applications \
--header 'Content-Type: application/json' \
--data '
{
"allowed_documents": [
"<string>"
],
"applicant_email": "<string>",
"applicant_phone": "<string>",
"client_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>",
"locale": "<string>",
"max_attempts": 123,
"metadata": "<unknown>",
"profile": "<unknown>",
"redirect_url": "<string>",
"require_video": true,
"ttl_hours": 123
}
'import requests
url = "http://localhost:8080/api/v1/kyc/applications"
payload = {
"allowed_documents": ["<string>"],
"applicant_email": "<string>",
"applicant_phone": "<string>",
"client_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>",
"locale": "<string>",
"max_attempts": 123,
"metadata": "<unknown>",
"profile": "<unknown>",
"redirect_url": "<string>",
"require_video": True,
"ttl_hours": 123
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
allowed_documents: ['<string>'],
applicant_email: '<string>',
applicant_phone: '<string>',
client_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
external_id: '<string>',
locale: '<string>',
max_attempts: 123,
metadata: '<unknown>',
profile: '<unknown>',
redirect_url: '<string>',
require_video: true,
ttl_hours: 123
})
};
fetch('http://localhost:8080/api/v1/kyc/applications', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "8080",
CURLOPT_URL => "http://localhost:8080/api/v1/kyc/applications",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'allowed_documents' => [
'<string>'
],
'applicant_email' => '<string>',
'applicant_phone' => '<string>',
'client_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'external_id' => '<string>',
'locale' => '<string>',
'max_attempts' => 123,
'metadata' => '<unknown>',
'profile' => '<unknown>',
'redirect_url' => '<string>',
'require_video' => true,
'ttl_hours' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://localhost:8080/api/v1/kyc/applications"
payload := strings.NewReader("{\n \"allowed_documents\": [\n \"<string>\"\n ],\n \"applicant_email\": \"<string>\",\n \"applicant_phone\": \"<string>\",\n \"client_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"external_id\": \"<string>\",\n \"locale\": \"<string>\",\n \"max_attempts\": 123,\n \"metadata\": \"<unknown>\",\n \"profile\": \"<unknown>\",\n \"redirect_url\": \"<string>\",\n \"require_video\": true,\n \"ttl_hours\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://localhost:8080/api/v1/kyc/applications")
.header("Content-Type", "application/json")
.body("{\n \"allowed_documents\": [\n \"<string>\"\n ],\n \"applicant_email\": \"<string>\",\n \"applicant_phone\": \"<string>\",\n \"client_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"external_id\": \"<string>\",\n \"locale\": \"<string>\",\n \"max_attempts\": 123,\n \"metadata\": \"<unknown>\",\n \"profile\": \"<unknown>\",\n \"redirect_url\": \"<string>\",\n \"require_video\": true,\n \"ttl_hours\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://localhost:8080/api/v1/kyc/applications")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"allowed_documents\": [\n \"<string>\"\n ],\n \"applicant_email\": \"<string>\",\n \"applicant_phone\": \"<string>\",\n \"client_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"external_id\": \"<string>\",\n \"locale\": \"<string>\",\n \"max_attempts\": 123,\n \"metadata\": \"<unknown>\",\n \"profile\": \"<unknown>\",\n \"redirect_url\": \"<string>\",\n \"require_video\": true,\n \"ttl_hours\": 123\n}"
response = http.request(request)
puts response.read_body{
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"link_expires_at": "2023-11-07T05:31:56Z",
"link_url": "<string>",
"status": "CREATED",
"client_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>"
}Body
A business asking for a KYC link.
Restrict which documents the applicant may present (id_card,
passport). Omit to allow both.
Delivery hints, so you can send the link yourself.
Link this verification to an existing Dinari customer, if there is one.
Your own identifier for this applicant. Echoed on every webhook.
Applicant's language for the hosted flow: fr (default), ar or en.
How many times the applicant may re-submit on this link. Default 3.
Opaque bag echoed back on every webhook.
Applicant-supplied identity to cross-check against the document
(first_name, last_name, birth_date, address, …).
Where to send the applicant once they finish.
Require a video capture. Defaults to true.
Turning it off means the applicant may send a single still — and a still can never establish liveness, so such a case can never be auto-approved and always lands in review. Only set false for a flow where a human confirms the holder by other means.
Link lifetime in hours. Defaults to Verifi's configured TTL.
Response
Link created
Response to a business creating an application.
Give this to your customer. It is valid until link_expires_at.
Where an application is in its life.
Mirrors Verifi's session vocabulary rather than inventing a Dinari one: a translation layer between two enums that mean the same thing is a place for them to drift apart.
CREATED, OPENED, SUBMITTED, PROCESSING, COMPLETED, EXPIRED, CANCELED, FAILED