Skip to main content

KYC Integration with Sumsub A Developer's Guide

00:04:30:00

KYC Is a Compliance Requirement, Not a Feature

Every fintech platform that handles money needs KYC (Know Your Customer) verification. It's mandated by regulators across jurisdictions — CySEC, FCA, ASIC, and others. The goal is to verify that clients are who they claim to be, prevent money laundering, and comply with anti-fraud regulations.

The worst thing you can do is build KYC verification yourself. Document verification, liveness detection, and sanctions screening are specialized problems. Use a provider like Sumsub, Onfido, or Jumio. They handle the hard parts — you handle the integration.

Here's how we integrate Sumsub across our fintech platforms.

Architecture Overview

Client Browser
    │
    ▼
Sumsub Web SDK (iframe/modal)
    │
    ▼
Sumsub Servers (verification)
    │
    ├──▶ Webhook → Your Backend → Update KYC Status
    │
    └──▶ Dashboard (manual review if needed)

The flow is:

  1. Client clicks "Verify Identity" on your platform
  2. You generate a Sumsub access token via their API
  3. Sumsub's SDK opens in the client's browser
  4. Client uploads documents and completes liveness check
  5. Sumsub processes and sends a webhook with the result
  6. Your backend updates the client's KYC status

Generating Access Tokens

Each verification session needs a unique token tied to the client:

php
class SumsubService
{
    public function createAccessToken(Client $client): string
    {
        $timestamp = time();
        $path = "/resources/accessTokens?userId={$client->id}&levelName=basic-kyc";

        $signature = hash_hmac(
            'sha256',
            $timestamp . 'POST' . $path,
            config('services.sumsub.secret_key')
        );

        $response = Http::withHeaders([
            'X-App-Token' => config('services.sumsub.app_token'),
            'X-App-Access-Sig' => $signature,
            'X-App-Access-Ts' => $timestamp,
        ])->post("https://api.sumsub.com{$path}");

        return $response->json('token');
    }
}

The levelName parameter maps to verification levels configured in your Sumsub dashboard. You might have:

  • basic-kyc — ID document + selfie (for standard accounts)
  • enhanced-kyc — proof of address + source of funds (for high-value accounts)

KYC State Machine

KYC status should be modeled as a state machine with clear transitions:

php
enum KycStatus: string
{
    case NotStarted = 'not_started';
    case Pending = 'pending';
    case DocumentsSubmitted = 'documents_submitted';
    case UnderReview = 'under_review';
    case Approved = 'approved';
    case Rejected = 'rejected';
    case Expired = 'expired';
}

State transitions are enforced:

php
class KycStateMachine
{
    private array $transitions = [
        'not_started' => ['pending'],
        'pending' => ['documents_submitted'],
        'documents_submitted' => ['under_review', 'approved', 'rejected'],
        'under_review' => ['approved', 'rejected'],
        'rejected' => ['pending'],  // Allow resubmission
        'expired' => ['pending'],   // Allow re-verification
    ];

    public function canTransition(KycStatus $from, KycStatus $to): bool
    {
        return in_array($to->value, $this->transitions[$from->value] ?? []);
    }
}

Webhook Processing

Sumsub sends webhooks for every status change. Process them reliably:

php
class SumsubWebhookController
{
    public function handle(Request $request): Response
    {
        // Verify webhook signature
        $signature = hash_hmac(
            'sha256',
            $request->getContent(),
            config('services.sumsub.webhook_secret')
        );

        if (!hash_equals($signature, $request->header('X-Payload-Digest'))) {
            return response('Invalid signature', 401);
        }

        // Process asynchronously
        ProcessKycWebhook::dispatch($request->all());

        return response('OK', 200);
    }
}

class ProcessKycWebhook implements ShouldQueue
{
    public function handle(): void
    {
        $applicantId = $this->data['applicantId'];
        $reviewStatus = $this->data['reviewResult']['reviewAnswer'];

        $client = Client::where('sumsub_applicant_id', $applicantId)->first();

        match ($reviewStatus) {
            'GREEN' => $this->approveClient($client),
            'RED' => $this->rejectClient($client, $this->data['reviewResult']),
            'YELLOW' => $this->flagForManualReview($client),
        };
    }

    private function approveClient(Client $client): void
    {
        $client->updateKycStatus(KycStatus::Approved);

        // Unlock platform features
        $client->enableDeposits();
        $client->enableTrading();

        // Notify client
        KycApproved::dispatch($client);
    }
}

Platform Access Control

Tie feature access to KYC status via middleware:

php
class RequireKycApproval
{
    public function handle(Request $request, Closure $next): Response
    {
        $client = $request->user();

        if ($client->kyc_status !== KycStatus::Approved) {
            return response()->json([
                'error' => 'KYC verification required',
                'kyc_status' => $client->kyc_status,
                'message' => $this->getMessage($client->kyc_status),
            ], 403);
        }

        return $next($request);
    }

    private function getMessage(KycStatus $status): string
    {
        return match ($status) {
            KycStatus::NotStarted => 'Please complete identity verification',
            KycStatus::Pending => 'Verification in progress',
            KycStatus::Rejected => 'Please resubmit your documents',
            default => 'Verification required',
        };
    }
}

Apply this middleware to sensitive routes:

php
Route::middleware('kyc.approved')->group(function () {
    Route::post('/deposits', [DepositController::class, 'store']);
    Route::post('/withdrawals', [WithdrawalController::class, 'store']);
    Route::post('/trading/orders', [OrderController::class, 'store']);
});

Document Storage and Privacy

KYC documents contain sensitive personal data. Handle with care:

  • Don't store documents locally — let Sumsub retain them. Query their API when you need to view documents.
  • If you must store — use encrypted S3 buckets with strict IAM policies
  • Retention policies — GDPR requires deletion upon request, but regulatory requirements may mandate 5-7 year retention
  • Access logging — every time someone views a client's KYC documents, log who accessed what and when

Key Takeaways

  1. Never build KYC verification yourself — use Sumsub, Onfido, or Jumio
  2. Model KYC as a state machine — clear states and enforced transitions
  3. Process webhooks asynchronously with signature verification
  4. Tie platform access to KYC status via middleware — deposits, trading, and withdrawals require approval
  5. Handle document privacy carefully — encryption, access logging, and retention policies
  6. Support resubmission — rejected clients should be able to try again easily

KYC integration is where compliance meets user experience. A smooth verification flow reduces client abandonment, while robust backend processing keeps regulators happy.

Frequently Asked Questions

Why use a third-party KYC provider instead of building in-house?

KYC providers like Sumsub handle document verification, liveness detection, sanctions screening, and regulatory updates across 200+ countries. Building this in-house would take years and require constant maintenance for compliance changes.

How long does KYC verification typically take?

Automated KYC verification through providers like Sumsub typically completes in 1-5 minutes for straightforward cases. Complex cases requiring manual review may take 24-48 hours.

What KYC levels are required for forex brokers?

Most jurisdictions require identity verification (government ID), proof of address, and source of funds documentation. Some regulators also require enhanced due diligence for high-value clients.

Want to discuss this topic or work together? Get in touch.

Contact me

Related Articles

agile-leadership-in-fintech-teams

api-design-for-fintech-platforms

building-scalable-fintech-crms-with-laravel