Identity verification (KYC)
Verify a customer's identity with a selfie and an ID document, or with a selfie alone when Cowdi already holds a recent verification.
Before a customer can transact, a KYC application verifies who they are. Your app captures a selfie and, usually, a photo of an ID document, and sends them to Cowdi. Cowdi checks them with Smile ID and tells you the result.
A returning customer doesn't always need to show their ID again. When Smile ID already holds an approved verification for them from the last 150 days, and that verification still has their identity details and ID image, a selfie alone is enough. This guide shows how to tell which case you're in and what to do if you get it wrong.
Who makes these calls
These routes act for the signed-in customer, so call them with the customer's own access
token. They submit images server-side, using the server_upload channel.
1. Create or get the application
POST /v1/kyc/applications creates the
customer's application. If they already have one in progress, you get that one back
instead, so it is safe to call every time. A 409 Conflict means a prospect is already
linked to a different user, or the caller's phone belongs to a merchant in another
organization.
curl -X POST https://api.cowdi.co/v1/kyc/applications \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "user_type": "INDIVIDUAL" }'Keep the id from the response. The next steps use it.
2. Check what to capture
Before you open the camera, ask Cowdi what this customer needs to capture with
GET /v1/kyc/applications/{id}/verification.
curl https://api.cowdi.co/v1/kyc/applications/$APPLICATION_ID/verification \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "X-SmileID-Api-Versions: v2"{
"requires_selfie": true,
"requires_liveness": false,
"requires_document": false,
"requires_consent": false,
"consent_notice": null,
"min_liveness_images": 0,
"max_liveness_images": 12,
"missing_applicant_fields": [],
"supported_documents": [
{
"country": "KE",
"id_types": [
{ "code": "IDENTITY_CARD", "label": "National ID", "has_back": true },
{ "code": "PASSPORT", "label": "Passport", "has_back": false }
]
}
]
}requires_document decides which screens to show:
true: capture a selfie and an ID document. Build the document picker fromsupported_documents, and capture the back of the ID whenhas_backistrue.false: Smile ID already holds a recent approved verification for this customer with their identity details and ID image. Capture only the selfie and skip the document screens.
Fetch the requirements each time you start a capture rather than caching them. The answer can change, for example when a customer's earlier verification becomes older than 150 days.
X-SmileID-Api-Versions lists the Smile ID versions your app's SDK can run. These examples
send v2. If you list v3, requires_liveness and requires_consent come back true, and
the capture must also carry liveness_images and consent. The reference for
POST /v1/kyc/applications/{id}/verification
explains both.
3. Submit the capture
Send the images to
POST /v1/kyc/applications/{id}/verification
with channel: "server_upload". Images are base64 strings, raw or as data: URLs. Send the
same X-SmileID-Api-Versions value you sent in step 2.
When a document is required, send the selfie, the document type and the document photos:
{
"channel": "server_upload",
"selfie": "data:image/jpeg;base64,/9j/4AAQ...",
"id_type": "IDENTITY_CARD",
"country": "KE",
"document_front": "data:image/jpeg;base64,/9j/4AAQ...",
"document_back": "data:image/jpeg;base64,/9j/4AAQ..."
}When requires_document is false, send the selfie alone. Leave out id_type and every
document_* field:
{
"channel": "server_upload",
"selfie": "data:image/jpeg;base64,/9j/4AAQ..."
}Either way, Cowdi answers 202 and the application moves to ID_VERIFICATION_PENDING.
4. Handle a missing document
If you send a selfie alone but the customer still needs to show an ID, Cowdi refuses the
capture with a 400 and starts nothing. This can happen if you skipped step 2 or reused an
old answer from it. The error tells you exactly what's missing:
{
"code": "INVALID_PARAMS",
"description": "document_front is required: no recent approved Smile ID enrollment with a complete identity and ID image for this applicant",
"invalid_params": [
{
"path": "document_front",
"reason": "required unless GET /verification answers requires_document: false",
"sub_code": "DOCUMENT_REQUIRED"
}
]
}Branch on sub_code, not on description, which is for people and can change. When you
see DOCUMENT_REQUIRED, show the document screens and submit again with the document:
const res = await fetch(`${API}/v1/kyc/applications/${applicationId}/verification`, {
method: "POST",
headers: {
Authorization: `Bearer ${customerToken}`,
"Content-Type": "application/json",
"X-SmileID-Api-Versions": "v2"
},
body: JSON.stringify({ channel: "server_upload", selfie })
});
if (res.status === 400) {
const error = await res.json();
const needsDocument = error.invalid_params?.some(
(param) => param.sub_code === "DOCUMENT_REQUIRED"
);
if (needsDocument) {
// Ask for the ID, then submit the selfie again with id_type and document_front.
return showDocumentCapture();
}
}5. Get the result
Smile ID's answer arrives a little later. Follow it with
GET /v1/kyc/sse, which streams every change to the
application, or check
GET /v1/kyc/applications/{id}/status.
| Status | What it means |
|---|---|
ID_VERIFICATION_PENDING | Waiting for Smile ID. |
PENDING_MANUAL_REVIEW | Smile ID couldn't decide, so a Cowdi reviewer will. |
APPROVED | The customer is verified. |
REJECTED | The verification failed. |
EXPIRED | No result arrived in time. Reopen it with POST /v1/kyc/applications/{id}/re-verify. |