All endpoints require an API key as a Bearer Authorization header. API keys must only be used in backend server code.
Integration Steps
Create a Verification Session
Your backend calls the Predicate API to create a KYC or KYB session
Display the QR Code and Link
Show the QR code or redirect URL to the user in your application
Monitor Session Status
Use SSE (recommended) or polling to track verification progress
Verify Wallet Addresses
Check if a wallet belongs to a verified identity before transactions
Step 1: Create a Verification Session
Your backend creates a session by calling the registration endpoint. The response includes a QR code and redirect URL for the user.
Individual (KYC)
Business (KYB)
curl -X POST "https://api.kyc.predicate.io/api/v1/register/individual?size=300" \
-H "Authorization: Bearer $PREDICATE_KYC_API_KEY "
curl -X POST "https://api.kyc.predicate.io/api/v1/register/business?size=300" \
-H "Authorization: Bearer $PREDICATE_KYC_API_KEY "
Response:
{
"sessionId" : "550e8400-e29b-41d4-a716-446655440000" ,
"redirectUrl" : "https://identity.predicate.io/session/550e8400-e29b-41d4-a716-446655440000" ,
"qrCode" : "data:image/png;base64,iVBORw0KGgoAAAANSU..."
}
Backend Implementation (Node.js)
const express = require ( 'express' );
const router = express . Router ();
const KYC_API_KEY = process . env . KYC_API_KEY ;
router . post ( '/kyc/create-session' , async ( req , res ) => {
const { type } = req . body ; // 'individual' or 'business'
const endpoint = type === 'business' ? 'business' : 'individual' ;
const url = `https://api.kyc.predicate.io/api/v1/register/ ${ endpoint } ` ;
try {
const response = await fetch ( url , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ KYC_API_KEY } `
}
});
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( error . message || `API error: ${ response . status } ` );
}
const data = await response . json ();
res . json ( data );
} catch ( error ) {
console . error ( 'Session creation error:' , error );
res . status ( 500 ). json ({ error: 'Failed to create verification session' });
}
});
module . exports = router ;
Step 2: Display the QR Code and Link
Display the base64-encoded QR code from the response directly in an <img> tag.
< div class = "kyc-section" >
< h2 > Complete Your Verification </ h2 >
< p > Scan the QR code or click the link below to start: </ p >
<!-- QR Code (from API response) -->
< img id = "kyc-qr" alt = "Verify Identity" style = "width: 300px; height: 300px;" />
<!-- Direct link -->
< p >< a id = "kyc-link" target = "_blank" > Verify Your Identity </ a ></ p >
</ div >
< script >
async function initiateKYC ( type ) {
const response = await fetch ( '/kyc/create-session' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ type: type || 'individual' })
});
const data = await response . json ();
// Display QR code (already base64-encoded with data URI prefix)
document . getElementById ( 'kyc-qr' ). src = data . qrCode ;
document . getElementById ( 'kyc-link' ). href = data . redirectUrl ;
// Start monitoring for completion
startMonitoring ( data . sessionId , type || 'individual' );
}
</ script >
What happens on the verification portal:
User lands on the verification page
User connects their wallet(s) - up to 20 across different chains
User completes identity verification
Session status updates are sent via SSE
Step 3: Monitor Session Status
Once a user starts verification, monitor their progress using SSE (recommended) or polling.
Session Status Values:
Status Description pendingSession created, user hasn’t started yet in_progressUser is actively completing verification completedVerification successful, user is verified failedVerification failed or was rejected
Option A: Real-Time Updates (SSE)
Server-Sent Events provide instant notifications when verification status changes.
The SSE endpoint requires your API key. Since browsers’ EventSource API doesn’t support custom headers, you must create a backend proxy.
Backend SSE Proxy (Node.js)
router . get ( '/kyc/stream/:type/:sessionId' , async ( req , res ) => {
const { type , sessionId } = req . params ;
if ( type !== 'individual' && type !== 'business' ) {
return res . status ( 400 ). json ({ error: 'Type must be individual or business' });
}
// Validate UUID format
const uuidRegex = / ^ [ 0-9a-f ] {8} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {12} $ / i ;
if ( ! uuidRegex . test ( sessionId )) {
return res . status ( 400 ). json ({ error: 'Invalid session ID format' });
}
// Set SSE headers
res . setHeader ( 'Content-Type' , 'text/event-stream' );
res . setHeader ( 'Cache-Control' , 'no-cache' );
res . setHeader ( 'Connection' , 'keep-alive' );
res . setHeader ( 'X-Accel-Buffering' , 'no' );
const kycUrl = `https://api.kyc.predicate.io/api/v1/status/realtime/ ${ type } / ${ sessionId } ` ;
try {
const response = await fetch ( kycUrl , {
headers: { 'Authorization' : `Bearer ${ KYC_API_KEY } ` }
});
if ( ! response . ok ) {
throw new Error ( `KYC API error: ${ response . status } ` );
}
const reader = response . body . getReader ();
const decoder = new TextDecoder ();
req . on ( 'close' , () => reader . cancel ());
while ( true ) {
const { done , value } = await reader . read ();
if ( done ) break ;
res . write ( decoder . decode ( value , { stream: true }));
}
} catch ( error ) {
console . error ( 'SSE streaming error:' , error );
res . write ( `event: error \n data: ${ JSON . stringify ({ error: 'Stream failed' }) } \n\n ` );
}
});
class VerificationMonitor {
constructor ( sessionId , type ) {
this . sessionId = sessionId ;
this . type = type ;
this . eventSource = null ;
}
start ( onStatusUpdate , onError ) {
// Connect to YOUR backend, not directly to KYC API
const url = `/kyc/stream/ ${ this . type } / ${ this . sessionId } ` ;
this . eventSource = new EventSource ( url );
this . eventSource . onmessage = ( event ) => {
const status = JSON . parse ( event . data );
onStatusUpdate ( status );
if ( status . status === 'completed' || status . status === 'failed' ) {
this . stop ();
}
};
this . eventSource . onerror = () => {
onError ( 'Connection lost' );
this . stop ();
};
}
stop () {
if ( this . eventSource ) {
this . eventSource . close ();
this . eventSource = null ;
}
}
}
function startMonitoring ( sessionId , type ) {
const monitor = new VerificationMonitor ( sessionId , type );
monitor . start (
( status ) => updateVerificationUI ( status ),
( error ) => console . error ( 'Monitoring error:' , error )
);
return monitor ;
}
SSE Event Format:
{
"sessionId" : "550e8400-e29b-41d4-a716-446655440000" ,
"status" : "completed" ,
"userId" : "user-uuid-here" ,
"completedAt" : "2025-01-21T10:30:00Z" ,
"verifiedWallets" : [
{ "id" : "wallet-uuid-1" , "address" : "0x1234..." , "chain" : "ethereum" },
{ "id" : "wallet-uuid-2" , "address" : "0x5678..." , "chain" : "polygon" }
],
"unverifiedWallets" : []
}
Wallet Aggregation: When the same person completes multiple verification sessions (detected via biometric deduplication), all their wallets are aggregated under the same user. A completed session returns wallets from ALL of that user’s verified sessions.
Option B: Polling
If SSE isn’t suitable, poll the status endpoint at regular intervals.
Backend Polling Endpoint (Node.js)
router . get ( '/kyc/status/:type/:sessionId' , async ( req , res ) => {
const { type , sessionId } = req . params ;
if ( type !== 'individual' && type !== 'business' ) {
return res . status ( 400 ). json ({ error: 'Type must be individual or business' });
}
const uuidRegex = / ^ [ 0-9a-f ] {8} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {12} $ / i ;
if ( ! uuidRegex . test ( sessionId )) {
return res . status ( 400 ). json ({ error: 'Invalid session ID format' });
}
const kycUrl = `https://api.kyc.predicate.io/api/v1/status/ ${ type } / ${ sessionId } ` ;
try {
const response = await fetch ( kycUrl , {
headers: { 'Authorization' : `Bearer ${ KYC_API_KEY } ` }
});
if ( ! response . ok ) {
throw new Error ( `KYC API error: ${ response . status } ` );
}
const data = await response . json ();
res . json ( data );
} catch ( error ) {
console . error ( 'Status check error:' , error );
res . status ( 500 ). json ({ error: 'Failed to check verification status' });
}
});
class VerificationPoller {
constructor ( sessionId , type , interval = 3000 ) {
this . sessionId = sessionId ;
this . type = type ;
this . interval = interval ;
this . isPolling = false ;
}
start ( onStatusUpdate , onError ) {
this . onStatusUpdate = onStatusUpdate ;
this . onError = onError ;
this . isPolling = true ;
this . poll ();
}
async poll () {
if ( ! this . isPolling ) return ;
try {
const response = await fetch ( `/kyc/status/ ${ this . type } / ${ this . sessionId } ` );
const status = await response . json ();
this . onStatusUpdate ( status );
if ( status . status === 'completed' || status . status === 'failed' ) {
this . stop ();
return ;
}
this . pollTimer = setTimeout (() => this . poll (), this . interval );
} catch ( error ) {
this . onError ( error );
this . pollTimer = setTimeout (() => this . poll (), this . interval );
}
}
stop () {
this . isPolling = false ;
if ( this . pollTimer ) clearTimeout ( this . pollTimer );
}
}
Verifying Wallet Addresses
Once a user has completed verification, check if any wallet address belongs to a verified identity.
curl -X POST "https://api.kyc.predicate.io/api/v1/verify" \
-H "Authorization: Bearer $PREDICATE_KYC_API_KEY " \
-H "Content-Type: application/json" \
-d '{
"address": "0x1234567890abcdef...",
"type": "individual"
}'
Response (verified):
{
"address" : "0x1234..." ,
"verified" : true ,
"userId" : "uuid-of-verified-identity" ,
"checkType" : "kyc" ,
"verifiedAt" : "2025-01-21T10:30:00Z" ,
"wallets" : [
{ "address" : "0x1234..." , "chain" : "ethereum" },
{ "address" : "0x5678..." , "chain" : "polygon" }
]
}
Response (not verified):
{
"address" : "0x1234..." ,
"verified" : false ,
"userId" : null ,
"checkType" : null ,
"verifiedAt" : null ,
"wallets" : []
}
Best Practices
Security
Never expose API keys in frontend code - All authenticated operations must happen on your backend
Use environment variables - Store API keys in .env files or secure secret management systems
Validate session IDs - Always validate UUID format before making API calls
User Experience
Show clear progress indicators - Update the UI as session status changes
Handle all statuses - Display appropriate messages for pending, in_progress, completed, and failed
Provide both QR and link - Some users prefer clicking, others prefer scanning
Use SSE over polling - Real-time updates are more efficient and user-friendly
Implement reconnection logic - Handle network interruptions gracefully
Close connections when done - Stop monitoring once verification completes or fails