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:
- Client clicks "Verify Identity" on your platform
- You generate a Sumsub access token via their API
- Sumsub's SDK opens in the client's browser
- Client uploads documents and completes liveness check
- Sumsub processes and sends a webhook with the result
- Your backend updates the client's KYC status
Generating Access Tokens
Each verification session needs a unique token tied to the client:
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:
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:
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:
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:
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:
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
- Never build KYC verification yourself — use Sumsub, Onfido, or Jumio
- Model KYC as a state machine — clear states and enforced transitions
- Process webhooks asynchronously with signature verification
- Tie platform access to KYC status via middleware — deposits, trading, and withdrawals require approval
- Handle document privacy carefully — encryption, access logging, and retention policies
- 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.
