Cowdidocs

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 from supported_documents, and capture the back of the ID when has_back is true.
  • 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.

StatusWhat it means
ID_VERIFICATION_PENDINGWaiting for Smile ID.
PENDING_MANUAL_REVIEWSmile ID couldn't decide, so a Cowdi reviewer will.
APPROVEDThe customer is verified.
REJECTEDThe verification failed.
EXPIREDNo result arrived in time. Reopen it with POST /v1/kyc/applications/{id}/re-verify.

On this page