Documentation/API/KYC

KYC Service

Subject to change

API reference for the Know Your Customer (KYC) verification process for users.

VERTEX ENGINE

00

Table of Contents

  1. Introduction to KYC
  2. KYC States and User Verification
  3. Document Upload to NATS Object Storage
  4. Submitting KYC Documents
  5. Querying User Information

1. Introduction to KYC

Know Your Customer (KYC) is a critical process for verifying the identity of users. The KYC process is integrated into the user aggregate and affects the user's verified status.

KYC Process Flow

  1. User Registration: Personal entity user created - KYC state is Invalid
  2. Document Upload: User uploads documents to NATS object storage
  3. Document Submission: User submits document references - KYC state transitions to Pending
  4. Review Process: Admin reviews submitted documents
  5. Approval/Rejection: Admin approves or rejects KYC - KYC state becomes Approved or Rejected
  6. User Verification: Upon KYC approval, the user's verified field is set to true

2. KYC States and User Verification

KYC States

The system tracks KYC progress through the following states:

  • Invalid (0): Initial state, no KYC documents uploaded
  • Pending (1): KYC documents uploaded, awaiting review
  • Approved (2): KYC documents reviewed and approved
  • Rejected (3): KYC documents reviewed and rejected

User Verification

The verified field on the user aggregate indicates whether a user has completed KYC verification:

  • verified: false - User has not completed KYC verification
  • verified: true - User has successfully completed KYC verification (KYC state is Approved)

The verifiedAt timestamp is set when KYC is approved.

3. Document Upload to NATS Object Storage

KYC documents should be uploaded to NATS object storage before submitting the verification request. The API is storage-agnostic and accepts document storage keys from any storage solution, but NATS object storage is recommended for consistency across the platform.

Document Naming Convention

Documents should follow these naming patterns for consistency:

Identity Documents:

  • kyc_id_front_<user_id>_<timestamp> - Front of ID document
  • kyc_id_back_<user_id>_<timestamp> - Back of ID document (if applicable)
  • kyc_passport_<user_id>_<timestamp> - Passport (if using passport instead of ID)

Proof of Residence:

  • kyc_proof_of_residence_<user_id>_<timestamp> - Utility bill, bank statement, etc.

Command Line Upload Method

Upload multiple KYC documents(bash)
# Example: Upload multiple KYC documents
xargs -I {} nats obj put --name={} kyc_dropbox_<user_id> ./document.pdf << EOF
kyc_id_front_<user_id>_<timestamp>
kyc_id_back_<user_id>_<timestamp>
kyc_proof_of_residence_<user_id>_<timestamp>
EOF

Programmatic Access (JavaScript)

// 1. Connect to NATS and get object store
const userId = "550e8400-e29b-41d4-a716-446655440000";
const bucketName = `kyc_dropbox_${userId}`;
const os = await natsConn?.jetstream().views.os(bucketName);

2. Upload document with headers

import { MsgHdrsImpl } from "nats.ws";

async function uploadKYCDocument(os, userId, documentType, file) {
  const timestamp = Math.floor(Date.now() / 1000);
  let documentName = '';
  
  switch(documentType) {
    case 'ID_FRONT':
      documentName = `kyc_id_front_${userId}_${timestamp}`;
      break;
    case 'ID_BACK':
      documentName = `kyc_id_back_${userId}_${timestamp}`;
      break;
    case 'PASSPORT':
      documentName = `kyc_passport_${userId}_${timestamp}`;
      break;
    case 'PROOF_OF_RESIDENCE':
      documentName = `kyc_proof_of_residence_${userId}_${timestamp}`;
      break;
    default:
      throw new Error('Invalid document type');
  }
  
  const headers = new MsgHdrsImpl();
  headers.set("original_filename", file.name);
  
  try {
    const info = await os?.put(
      {
        name: documentName,
        headers,
      },
      fileToReadableStream(file)
    );
    console.log(`Uploaded ${file.name} as ${documentName}`);
    return documentName;
  } catch (err) {
    console.error("Upload error:", err);
    throw err;
  }
}

// Helper function to convert File to ReadableStream
function fileToReadableStream(file) {
  return new ReadableStream({
    async start(controller) {
      const reader = file.stream().getReader();
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          controller.enqueue(value);
        }
      } finally {
        controller.close();
      }
    }
  });
}

4. Submitting KYC Documents

After uploading documents to your storage solution, submit the document references to initiate the verification process.

Subject: svc.user.<partner_id>.upload_kyc_documents

Permissions Required: write

Request

{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",  // Required - UUID of the user
  "id_document": "string",                             // Optional - Storage key for ID document
  "proof_of_residence": "string"                       // Optional - Storage key for proof of residence
}

Important Notes

  • At least one document must be provided (either id_document or proof_of_residence)
  • Both documents can be provided, but only one is required
  • The storage keys should reference documents already uploaded to your storage solution
  • Documents can be re-uploaded if KYC is in Pending or Rejected state

Example Request:

{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "id_document": "kyc_id_front_550e8400-e29b-41d4-a716-446655440000_1726220326",
  "proof_of_residence": "kyc_proof_of_residence_550e8400-e29b-41d4-a716-446655440000_1726220276"
}

Response

{
  "success": true,
  "message": "KYC documents uploaded and submitted for verification"
}

Effects

  • KYC state transitions from Invalid to Pending
  • kycSubmittedAt timestamp is set
  • Emits KYCDocumentsUploaded event
  • Documents are stored in the user aggregate's kycDocuments map

Possible Errors

400 Bad Request - Missing Documents

{
  "error": {
    "code": 400,
    "message": "missing documents"
  }
}

Returned when no documents are provided.

400 Bad Request - Already Verified

{
  "error": {
    "code": 400,
    "message": "user already verified"
  }
}

Returned when the user is already verified or KYC is already approved.

5. Querying User Information

List Users

Get a list of users with their KYC status and verification details.

Subject: svc.user.<partner_id>.list

Permissions Required: read

Request

{
  "entity_id": "550e8400-e29b-41d4-a716-446655440000"  // Required - Entity ID to filter users
}

Response

{
  "users": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com",
      "phone_number": "+27123456789",
      "verified": true,
      "verified_at": "2024-08-01T15:30:45Z",
      "kyc_status": "approved",              // "pending", "approved", "rejected", or empty
      "kyc_submitted_at": "2024-08-01T14:00:00Z",
      "kyc_reviewed_at": "2024-08-01T15:30:45Z",
      "kyc_rejection_reason": "",            // Only populated if rejected
      "entity_id": "550e8400-e29b-41d4-a716-446655440000",
      "created_at": "2024-08-01T10:00:00Z",
      // ... other user fields
    }
  ]
}

Note

The kyc_status field returns string values ("pending", "approved", "rejected") for better readability, mapped from the internal numeric KYC state.