Skip to main content
All endpoints require an API key as a Bearer Authorization header. API keys must only be used in backend server code.

Integration Steps

1

Create a Verification Session

Your backend calls the Predicate API to create a KYC or KYB session
2

Display the QR Code and Link

Show the QR code or redirect URL to the user in your application
3

Monitor Session Status

Use SSE (recommended) or polling to track verification progress
4

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.
cURL
curl -X POST "https://api.kyc.predicate.io/api/v1/register/individual?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..."
}
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;
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:
StatusDescription
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.
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\ndata: ${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.
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
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

Performance

  • 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