RuonID SDK Documentation
Integrate privacy-preserving identity verification into your app. Two products, one SDK:
- Sybil Resistance Free. Verify each user is a unique real person. No personal data shared. Returns an anonymous, app-specific token.
- Identity Verification $0.15/check. Get verified identity data (name, DOB, nationality, etc.) encrypted end-to-end. Only you can decrypt it.
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
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 });
});
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.
| Method | Description |
|---|---|
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:
- Fetches the server's signing key from the JWKS endpoint
- Verifies the KMS signature on the receipt
- 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
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: 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:
| Field | Description | Source |
|---|---|---|
name | Full name | Passport MRZ |
nationality | Nationality | Passport MRZ |
dateOfBirth | Date of birth | Passport MRZ |
gender | Gender (M, F, or X) | Passport MRZ |
natIdNumber | National ID number | Passport DG11 (if available) |
countryCode | Country code | Passport MRZ |
documentNumber | Passport number | Passport MRZ |
expiryDate | Passport expiry date (YYMMDD) | Passport MRZ |
placeOfBirth | Place of birth | Passport DG11 (if available) |
passportPhoto | Passport 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.