> ## Documentation Index
> Fetch the complete documentation index at: https://docs.predicate.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration Guide

> Step-by-step guide to integrate Predicate Identity verification

<Warning>
  All endpoints require an API key as a Bearer `Authorization` header. API keys must only be used in backend server code.
</Warning>

## Integration Steps

<Steps>
  <Step title="Create a Verification Session">
    Your backend calls the Predicate API to create a KYC or KYB session
  </Step>

  <Step title="Display the QR Code and Link">
    Show the QR code or redirect URL to the user in your application
  </Step>

  <Step title="Monitor Session Status">
    Use SSE (recommended) or polling to track verification progress
  </Step>

  <Step title="Verify Wallet Addresses">
    Check if a wallet belongs to a verified identity before transactions
  </Step>
</Steps>

## 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.

<Tabs>
  <Tab title="Individual (KYC)">
    ```bash cURL theme={null}
    curl -X POST "https://api.identity.predicate.io/api/v1/register/individual?size=300" \
      -H "Authorization: Bearer $PREDICATE_KYC_API_KEY"
    ```
  </Tab>

  <Tab title="Business (KYB)">
    ```bash cURL theme={null}
    curl -X POST "https://api.identity.predicate.io/api/v1/register/business?size=300" \
      -H "Authorization: Bearer $PREDICATE_KYC_API_KEY"
    ```
  </Tab>
</Tabs>

**Request Body (Optional):**

```json theme={null}
{
  "walletAddress": "0x1234567890abcdef..."
}
```

<Note>
  **Wallet Constraint:** When you include `walletAddress` in the request:

  * The user can **only** connect and verify with that specific wallet address
  * If a session (`pending`, `retry`, or `completed`) already exists for that wallet with the same verification type, the existing session is returned (session reuse)
  * Sessions are only reused within the same check type (KYC sessions for `/register/individual`, KYB for `/register/business`)
  * Useful for pre-registering known wallet addresses or enforcing wallet constraints
</Note>

**HTTP Status Codes:**

* `201 Created` - A new session was created
* `200 OK` - An existing session was reused (when `walletAddress` matched an existing reusable session)

**Response:**

```json theme={null}
{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "redirectUrl": "https://identity.predicate.io/session/550e8400-e29b-41d4-a716-446655440000",
  "qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSU..."
}
```

<Accordion title="Backend Implementation (Node.js)">
  ```javascript theme={null}
  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, walletAddress } = req.body; // 'individual' or 'business', optional wallet

    const endpoint = type === 'business' ? 'business' : 'individual';
    const url = `https://api.identity.predicate.io/api/v1/register/${endpoint}`;

    // Build request options
    const options = {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${KYC_API_KEY}`
      }
    };

    // Include wallet address if provided (restricts session to this wallet)
    if (walletAddress) {
      options.headers['Content-Type'] = 'application/json';
      options.body = JSON.stringify({ walletAddress });
    }

    try {
      const response = await fetch(url, options);

      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;
  ```
</Accordion>

## Step 2: Display the QR Code and Link

Display the base64-encoded QR code from the response directly in an `<img>` tag.

```html theme={null}
<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 per session, 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                                           |
| ----------- | ----------------------------------------------------- |
| `pending`   | Session created, user in verification process         |
| `submitted` | Documents submitted, awaiting manual operator review  |
| `completed` | Verification successful, user is verified             |
| `rejected`  | Verification permanently failed (fraud, sanctions)    |
| `retry`     | Fixable issue (e.g., blurry document), user can retry |

<Warning>
  **Important:** A `submitted` status means documents are under manual review. The wallet is **not** verified until status becomes `completed`.
</Warning>

<Tabs>
  <Tab title="SSE (Recommended)">
    Server-Sent Events provide instant notifications when verification status changes.

    <Warning>
      The SSE endpoint requires your API key. Since browsers' `EventSource` API doesn't support custom headers, you must create a backend proxy.
    </Warning>

    <Note>
      **Proxy Timeout:** Ensure your backend proxy does not impose a response time limit on the SSE connection. Some frameworks (e.g., Next.js API routes) have default timeouts that will prematurely close the stream. Use a standalone server or configure your framework to allow long-lived connections.
    </Note>

    <Accordion title="Backend SSE Proxy (Node.js)">
      ```javascript theme={null}
      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.identity.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`);
        }
      });
      ```
    </Accordion>

    <Accordion title="Frontend SSE Client">
      ```javascript theme={null}
      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 === 'rejected') {
              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;
      }
      ```
    </Accordion>

    **SSE Event Format:**

    ```json theme={null}
    {
      "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 Arrays:**

    | Field               | Description                                                                           |
    | ------------------- | ------------------------------------------------------------------------------------- |
    | `verifiedWallets`   | Wallets connected to this session. Only populated when session status is `completed`. |
    | `unverifiedWallets` | Wallets connected to this session. Populated when session is not yet `completed`.     |
  </Tab>

  <Tab title="Polling">
    If SSE isn't suitable for your infrastructure, poll the status endpoint at regular intervals.

    <Accordion title="Backend Polling Endpoint (Node.js)">
      ```javascript theme={null}
      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.identity.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' });
        }
      });
      ```
    </Accordion>

    <Accordion title="Frontend Polling Client">
      ```javascript theme={null}
      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 === 'rejected') {
              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);
        }
      }
      ```
    </Accordion>
  </Tab>
</Tabs>

## Verifying Wallet Addresses

Once a user has completed verification, check if any wallet address belongs to a verified identity.

```bash cURL theme={null}
curl -X POST "https://api.identity.predicate.io/api/v1/verify" \
  -H "Authorization: Bearer $PREDICATE_KYC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "0x1234567890abcdef...",
    "type": "individual"
  }'
```

**Response (verified):**

```json theme={null}
{
  "address": "0x1234...",
  "verified": true,
  "status": "completed",
  "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):**

```json theme={null}
{
  "address": "0x1234...",
  "verified": false
}
```

<Note>
  **Verification Levels:** The verify endpoint returns verified status for wallets that were verified at your organization's current verification level. If you upgrade your verification level, previously verified wallets will need to re-verify. Wallets verified through other organizations at the same scrutiny level will also appear as verified.
</Note>

## 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
* **Verify session ownership** - The API ensures sessions can only be accessed by the customer that created them

### User Experience

* **Show clear progress indicators** - Update the UI as session status changes
* **Handle all statuses** - Display appropriate messages for pending, submitted, completed, rejected, and retry
* **Explain submitted status to users** - When status is `submitted`, inform users that their documents are under review and they'll be notified when a decision is made. `submitted` does NOT mean approved.
* **Provide both QR and link** - Some users prefer clicking, others prefer scanning
* **Consider timeouts** - Sessions don't expire, but you may want to implement client-side timeouts

### 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

## Complete Integration Example

Here's a full example tying everything together:

```html theme={null}
<!DOCTYPE html>
<html>
<head>
  <title>Identity Verification</title>
  <style>
    .kyc-container {
      max-width: 400px;
      margin: 50px auto;
      text-align: center;
      font-family: sans-serif;
    }
    .kyc-qr {
      width: 300px;
      height: 300px;
      border: 1px solid #ddd;
      margin: 20px auto;
    }
    .status-pending { color: #f59e0b; }
    .status-verified { color: #10b981; }
    .status-failed { color: #ef4444; }
  </style>
</head>
<body>
  <div class="kyc-container">
    <h1>Verify Your Identity</h1>

    <div id="start-section">
      <button id="start-btn" onclick="startVerification()">
        Start Verification
      </button>
    </div>

    <div id="verification-section" style="display: none;">
      <p>Scan the QR code or click the link below:</p>
      <img id="kyc-qr" class="kyc-qr" alt="QR Code" />
      <p><a id="kyc-link" target="_blank">Open Verification Portal →</a></p>
      <p id="verification-status" class="status-pending">Waiting...</p>
    </div>
  </div>

  <script>
    async function startVerification() {
      try {
        const response = await fetch('/kyc/create-session', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ type: 'individual' })
        });

        if (!response.ok) throw new Error('Failed to create session');

        const data = await response.json();

        document.getElementById('start-section').style.display = 'none';
        document.getElementById('verification-section').style.display = 'block';
        document.getElementById('kyc-qr').src = data.qrCode;
        document.getElementById('kyc-link').href = data.redirectUrl;

        const monitor = new VerificationMonitor(data.sessionId, 'individual');
        monitor.start(
          (status) => {
            const statusEl = document.getElementById('verification-status');
            switch (status.status) {
              case 'completed':
                statusEl.textContent = '✓ Verified!';
                statusEl.className = 'status-verified';
                break;
              case 'pending':
                statusEl.textContent = 'Verification in progress...';
                statusEl.className = 'status-pending';
                break;
              case 'submitted':
                statusEl.textContent = 'Under review...';
                statusEl.className = 'status-pending';
                break;
              case 'retry':
                statusEl.textContent = 'Please resubmit documents';
                statusEl.className = 'status-pending';
                break;
              case 'rejected':
                statusEl.textContent = '✗ Verification rejected';
                statusEl.className = 'status-failed';
                break;
            }
          },
          (error) => console.error('Monitor error:', error)
        );
      } catch (error) {
        console.error('Error:', error);
        alert('Failed to start verification');
      }
    }

    class VerificationMonitor {
      constructor(sessionId, type) {
        this.sessionId = sessionId;
        this.type = type;
        this.eventSource = null;
      }

      start(onStatusUpdate, onError) {
        const url = `/kyc/stream/${this.type}/${this.sessionId}`;
        this.eventSource = new EventSource(url);

        this.eventSource.onmessage = (event) => {
          try {
            const status = JSON.parse(event.data);
            onStatusUpdate(status);
            if (status.status === 'completed' || status.status === 'rejected') {
              this.stop();
            }
          } catch (e) {
            console.error('Parse error:', e);
          }
        };

        this.eventSource.onerror = () => {
          onError('Connection lost');
          this.stop();
        };
      }

      stop() {
        if (this.eventSource) {
          this.eventSource.close();
          this.eventSource = null;
        }
      }
    }
  </script>
</body>
</html>
```

## API Reference

| Method | Endpoint                                     | Purpose                       | Auth    |
| ------ | -------------------------------------------- | ----------------------------- | ------- |
| `POST` | `/api/v1/register/individual[?size=N]`       | Create individual KYC session | API Key |
| `POST` | `/api/v1/register/business[?size=N]`         | Create business KYB session   | API Key |
| `POST` | `/api/v1/verify`                             | Check if wallet is verified   | API Key |
| `GET`  | `/api/v1/status/{type}/{sessionId}`          | Get session status (one-shot) | API Key |
| `GET`  | `/api/v1/status/realtime/{type}/{sessionId}` | Stream session updates (SSE)  | API Key |

**Query Parameters for Registration Endpoints:**

* `size` (optional): QR code size in pixels, between 80 and 2000. Default: 300.

All API endpoints require a Bearer token:

```
Authorization: Bearer YOUR_API_KEY
```
