RuonID SDK Documentation

Integrate privacy-preserving identity verification into your app. Two products, one SDK:

Installation

npm install @ruonid/sdk

The SDK runs server-side (Node.js). Your frontend generates QR codes or deeplinks, your backend handles the encrypted callback.

Quick Start

1. Initialize the client

import { RuonID } from '@ruonid/sdk';

const ruonid = new RuonID(process.env.RUONID_PRIVATE_KEY);

2. Create a verification session

// Free sybil resistance
const session = ruonid.createVerifySession('https://yourapp.com/api/callback');

// Or full identity verification (paid)
const session = ruonid.createSession('https://yourapp.com/api/callback', {
  requestedFields: ['name', 'nationality', 'dateOfBirth'],
});

3. Display the QR code

// Generate a QR code URL for the user to scan with RuonID
const qrData = RuonID.toDeepLink(session);
// Render qrData as a QR code in your UI

4. Handle the callback

// Sybil resistance
app.post('/api/callback', async (req, res) => {
  const { appSpecificId, deviceVerified, receipt } = await ruonid.handleVerifyCallback(req.body);
  // appSpecificId: unique anonymous ID for this user in your app
  // Same user always gets the same ID. Different apps get different IDs
  res.json({ ok: true });
});

// Identity verification
app.post('/api/callback', async (req, res) => {
  const identity = await ruonid.handleCallback(req.body);
  console.log(identity.name);         // "John Doe"
  console.log(identity.nationality);  // "US"
  console.log(identity.dateOfBirth);  // "1990-01-15"
  res.json({ ok: true });
});

Keypair Setup

RuonID uses secp256k1 keypairs for authentication. Generate one and store the private key securely:

const { publicKey, privateKey } = RuonID.generateKeypair();

// Store privateKey securely (env variable, secrets manager)
// Register publicKey at ruonlabs.com/developers
Your private key signs every request. Never expose it to the client. The public key is your developer identifier. Register it on the developer console.

Sybil Resistance Free

Verify each user is a unique real person without collecting any personal information. Each user gets a deterministic anonymous ID that's consistent across sessions but can't be linked across apps.

// Create session
const session = ruonid.createVerifySession('https://yourapp.com/api/callback');
const qrData = RuonID.toDeepLink(session);

// Handle callback
app.post('/api/callback', async (req, res) => {
  const { appSpecificId, deviceVerified, receipt } = await ruonid.handleVerifyCallback(req.body);

  // appSpecificId: 0x + 64 hex chars
  // Same user + same developer = same ID every time
  // receipt.hashSigVerified: server-signed proof of device attestation

  res.json({ ok: true, userId: appSpecificId });
});

The sybil tier is free but requires developer registration. The callback is ECIES-encrypted. The SDK decrypts it automatically using your private key.

Identity Verification $0.15/check

Get verified identity data from the user's passport. The user sees a consent screen listing exactly which fields you're requesting. Data is encrypted on their device with your public key. Only your private key can decrypt it.

// Create session with specific fields
const session = ruonid.createSession('https://yourapp.com/api/callback', {
  requestedFields: ['name', 'nationality', 'dateOfBirth', 'gender'],
  includeAppSpecificId: true,  // Also get the anonymous sybil ID
});
const qrData = RuonID.toDeepLink(session);

// Handle encrypted callback
app.post('/api/callback', async (req, res) => {
  const identity = await ruonid.handleCallback(req.body, session.sessionId);

  console.log(identity.name);           // "John Doe"
  console.log(identity.nationality);    // "US"
  console.log(identity.dateOfBirth);    // "1990-01-15"
  console.log(identity.gender);         // "M"
  console.log(identity.appSpecificId);  // "0xabc..."
  console.log(identity.receipt);        // Server-signed attestation

  res.json({ ok: true });
});
Requires developer registration with business validation. Only fields you request are shared, nothing more.

Identity Migration

For passport-bound identities (users whose passport doesn't contain a national ID number), renewing their passport changes their anonymous ID. The migration flow lets them link their old ID to their new one so you can update your records. Users with a national ID in their passport have a permanent identity that doesn't change on renewal.

const session = ruonid.createMigrationSession('https://yourapp.com/api/migrate');
const qrData = RuonID.toDeepLink(session);

app.post('/api/migrate', async (req, res) => {
  const { oldAppSpecificId, newAppSpecificId } = await ruonid.handleMigrationCallback(req.body);
  // Update your database: oldAppSpecificId → newAppSpecificId
  res.json({ ok: true });
});

RuonID Class

new RuonID(privateKey: string)

Creates an SDK client. All API requests are authenticated by signing with this key.

MethodDescription
createVerifySession(callbackUrl, options?)Create a sybil resistance session Free
handleVerifyCallback(body)Decrypt and validate the sybil callback
createSession(callbackUrl, options?)Create an identity verification session Paid
handleCallback(body, sessionId?)Decrypt and validate the identity callback
createMigrationSession(callbackUrl)Create a migration session
handleMigrationCallback(body)Decrypt and validate the migration callback
rotateKey(newPrivateKey)Rotate your keypair (dual-signed)
sandboxSimulate(session)Test with sandbox (fake data)
static generateKeypair()Generate a new secp256k1 keypair
static toUniversalLink(session)Convert session to universal link URL
static toDeepLink(session)Convert session to deep link URL

Types

VerifyCallback (sybil)

{
  appSpecificId: string;        // 0x + 64 hex chars, unique per developer
  identityTier?: string;        // "unique" or "passport-bound"
  deviceVerified?: boolean;     // Device passed attestation
  receipt?: VerifiedReceipt;    // Server-signed proof
}

DecryptedIdentity (PII)

{
  appSpecificId?: string;       // If includeAppSpecificId was set
  sessionId: string;            // Matches the original session
  name?: string;
  nationality?: string;
  dateOfBirth?: string;
  gender?: string;              // "M", "F", or "X"
  placeOfBirth?: string;        // If available on passport chip
  natIdNumber?: string;
  countryCode?: string;
  documentNumber?: string;
  expiryDate?: string;          // Passport expiry (YYMMDD)
  passportPhoto?: string;       // Base64 JPEG from NFC chip
  identityTier?: string;
  receipt?: VerifiedReceipt;
}

MigrationCallback

{
  oldAppSpecificId: string;
  newAppSpecificId: string;
  identityTier?: string;
  receipt?: VerifiedReceipt;
}

Encryption

All callbacks use ECIES + AES-256-GCM encryption. The RuonID app generates a fresh AES key, encrypts the payload, then encrypts the AES key with your secp256k1 public key via ECIES. The callback body contains:

{
  sessionId: string;
  encryptedBlob: string;   // AES-256-GCM encrypted payload (hex)
  encryptedKey: string;    // AES key encrypted via ECIES (hex)
  userPublicKey: string;   // User's secp256k1 public key (hex)
  signature: string;       // secp256k1 signature over the above fields
}

The SDK's handle*Callback methods verify the signature, decrypt, and return clean data. You never need to handle the crypto manually.

Server Receipts

Every callback includes a server-signed receipt proving the RuonID server verified the user's device (App Attest on iOS, Play Integrity on Android). The SDK automatically:

  1. Fetches the server's signing key from the JWKS endpoint
  2. Verifies the KMS signature on the receipt
  3. Verifies the payload hash matches the callback data
// Receipt fields available after processing:
receipt.hashSigVerified   // true if KMS signature is valid
receipt.hashVerified      // true if payload hash matches
receipt.deviceVerified    // true if device passed attestation
receipt.developerPublicKey
receipt.timestamp
If you're verifying receipts without the SDK (e.g., in Python), note that the server uses AWS KMS which can produce high-S ECDSA signatures. Disable low-S enforcement: secp256k1.verify(sig, hash, key, { lowS: false })

QR Code Integration

The SDK provides two URL formats for QR codes:

// Deep link (for in-app QR scanners)
const qr = RuonID.toDeepLink(session);
// → ruonid://link?d=eJw9kMtO...

// Universal link (opens RuonID app or falls back to download page)
const qr = RuonID.toUniversalLink(session);
// → https://ruonlabs.com/link?d=eJw9kMtO...

Both formats use compressed payloads (deflate + base64url in the d query parameter) for smaller, more scannable QR codes.

Sandbox Testing

Test your integration without a real user or the RuonID app. The sandbox sends a properly encrypted callback with fake data to your server.

const session = ruonid.createSession('https://yourapp.com/api/callback', {
  requestedFields: ['name', 'nationality', 'dateOfBirth'],
});

// Instead of showing a QR, send to sandbox
await ruonid.sandboxSimulate(session);

// Your callback receives:
// name: "Oliver Brown"
// nationality: "United Kingdom"
// dateOfBirth: "1986-09-15"
Sandbox callbacks include sandbox: true so you can distinguish them from production. All three flows work in sandbox.

Key Rotation

Rotate your keypair without disrupting your users' app-specific IDs:

const newKeypair = RuonID.generateKeypair();
await ruonid.rotateKey(newKeypair.privateKey);
// SDK now uses the new key for all subsequent requests

Rotation is authenticated with dual signatures (old + new key) and a server-issued nonce. Drain in-flight requests before rotating.

Available Identity Fields

These fields can be requested in the identity verification flow:

FieldDescriptionSource
nameFull namePassport MRZ
nationalityNationalityPassport MRZ
dateOfBirthDate of birthPassport MRZ
genderGender (M, F, or X)Passport MRZ
natIdNumberNational ID numberPassport DG11 (if available)
countryCodeCountry codePassport MRZ
documentNumberPassport numberPassport MRZ
expiryDatePassport expiry date (YYMMDD)Passport MRZ
placeOfBirthPlace of birthPassport DG11 (if available)
passportPhotoPassport photo (base64 JPEG)Passport DG2

Fields from DG11 are optional. Not all passports include them. If a field isn't available on the user's passport, it's silently omitted from the response.