Chapter 01 / iOS SDK
Add PulseProof via Swift Package Manager in Xcode, or with CocoaPods. Requires iOS 16 minimum deployment target.
Swift Package Manager// Xcode: File > Add Package Dependencies
// Paste the repository URL:
https://github.com/pulseproof/ios-sdk
// Or add to Package.swift dependencies:
.package(
url: "https://github.com/pulseproof/ios-sdk",
from: "1.0.0"
)
// CocoaPods alternative, add to Podfile:
pod 'PulseProofSDK', '~> 1.0'
// Then run: pod install
Call configure once at app launch. Keys are scoped: use sandbox keys for testing, live keys for production.
Swift// AppDelegate.swift
import PulseProofSDK
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [...]?
) -> Bool {
PulseProof.configure(
apiKey: "pp_live_your_key_here",
environment: .production // or .sandbox
)
return true
}
}
Call verify from any view controller. The SDK handles camera permissions, Watch connectivity, ZK proof generation, and Secure Enclave signing.
Swiftimport PulseProofSDK
class CheckoutViewController: UIViewController {
func verifyUser() {
PulseProof.shared.verify(
tier: .zkSovereign, // .camera .watchFused .zkSovereign
from: self,
timeout: 30 // seconds before auto-cancel
) { [weak self] result in
switch result {
case .verified(let token):
self?.proceedWithToken(token)
case .cancelled:
break
case .failed(let error):
self?.showError(error.localizedDescription)
}
}
}
}
The verified token is a compact CBOR payload signed by the Secure Enclave. Post it to your server over HTTPS.
Swiftfunc proceedWithToken(_ token: PulseProofToken) {
var request = URLRequest(
url: URL(string: "https://api.yourapp.com/verify")!
)
request.httpMethod = "POST"
request.setValue(
"application/cbor",
forHTTPHeaderField: "Content-Type"
)
request.setValue(
token.sessionId,
forHTTPHeaderField: "X-PulseProof-Session"
)
request.httpBody = token.cbor // raw CBOR bytes, < 2KB
URLSession.shared
.dataTask(with: request) { data, response, error in
// handle your backend response
}
.resume()
}
One API call confirms the token is valid, unexpired, and meets your required trust tier. No biometric data ever reaches your servers.
Node.js// server.js
const express = require('express')
const { PulseProof } = require('@pulseproof/verify')
const app = express()
app.post('/verify',
express.raw({ type: 'application/cbor' }),
async (req, res) => {
const result = await PulseProof.verify(req.body, {
apiKey: process.env.PULSEPROOF_API_KEY,
minTier: 'zkSovereign', // enforce minimum trust level
maxAge: 300 // token must be under 5 min old
})
if (!result.verified) {
return res.status(401).json({ error: 'Liveness check failed' })
}
// result.tier · result.sessionId · result.timestamp
res.json({ verified: true, sessionId: result.sessionId })
}
)
appID, callback URL, and optional ttl (max 600 seconds). The relay returns a sessionID and a ready-to-open verifyURL. No API key required.window.location.href — never window.open(). Save the sessionID to sessionStorage first, because the page navigates away. In Safari on iOS, this triggers a Universal Link and the PulseProof app opens directly. In Chrome on iOS, the relay's /start page loads instead and shows an "Open PulseProof" button.appID and asks the user to approve. Once approved, biometric capture begins.yourapp.com/callback?session=ID&status=success&trustTier=1. Read trustTier directly from URL params — no polling needed for most use cases.{status, token, trustTier, expiresAt, trustScore}.cbor (Node) or cbor2 (Python). Check expiresAt. No API call to PulseProof needed — the token is self-authenticating via the embedded Groth16 ZK proof and Secure Enclave ECDSA signature.// ── Step 1: Create session and open verification ──
async function startPulseProofVerification() {
const resp = await fetch('https://verify.pulseproof.app/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appID: 'com.yourcompany.yourapp', // YOUR identifier — shown in iOS consent screen
callback: window.location.href, // current page handles the return
ttl: 600 // 10 min session
})
});
const { sessionID, verifyURL } = await resp.json();
// Save before navigating — page reloads on return
sessionStorage.setItem('pp_session', sessionID);
// window.location.href ONLY — window.open() does NOT trigger Universal Links
window.location.href = verifyURL;
}
// ── Step 2: On page load, detect return from PulseProof ──
(function checkReturn() {
const urlParams = new URLSearchParams(window.location.search);
const urlSession = urlParams.get('session');
const urlStatus = urlParams.get('status');
const urlTrustTier = urlParams.get('trustTier');
const storedSession = sessionStorage.getItem('pp_session');
const sessionID = storedSession || (urlStatus === 'success' ? urlSession : null);
if (!sessionID) return;
sessionStorage.removeItem('pp_session');
if (urlSession) window.history.replaceState({}, '', window.location.pathname);
// Fast path: trustTier already in URL — save immediately
if (urlStatus === 'success' && urlTrustTier) {
saveVerified({ trustTier: parseInt(urlTrustTier) });
return;
}
// Slow path: poll relay for full CBOR token
pollForToken(sessionID).then(saveVerified);
})();
// ── Poll relay until complete ──
async function pollForToken(sessionID, { intervalMs = 2000, timeoutMs = 600_000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, intervalMs));
const resp = await fetch(`https://verify.pulseproof.app/api/sessions/${sessionID}`);
if (resp.status === 404) throw new Error('Session expired');
const data = await resp.json();
if (data.status === 'complete') return data; // { token, trustTier, expiresAt, trustScore }
if (data.status === 'error') throw new Error(data.error);
}
throw new Error('Verification timed out');
}
// ── Save verified state ──
function saveVerified({ trustTier }) {
// Send to your backend, update UI, etc.
console.log('Verified! trustTier:', trustTier);
}
// No API key needed — decode the CBOR token directly
// npm install cbor
const cbor = require('cbor');
app.post('/api/verify-human', async (req, res) => {
const { token } = req.body;
if (!token) return res.status(400).json({ ok: false, reason: 'missing token' });
// Decode the CBOR proof token
const payload = cbor.decode(Buffer.from(token, 'base64url'));
// Check expiry
if (payload.expiresAt < Math.floor(Date.now() / 1000)) {
return res.status(401).json({ ok: false, reason: 'token expired' });
}
// Enforce minimum trust tier (optional)
// 1 = Camera rPPG, 2 = Watch+Camera, 3 = zkSovereign
if (payload.trustTier < 1) {
return res.status(401).json({ ok: false, reason: 'insufficient trust tier' });
}
// Mark user as verified in your DB
await db.users.update({ verified: true, trustTier: payload.trustTier });
res.json({ ok: true, trustTier: payload.trustTier, expiresAt: payload.expiresAt });
});
// ── Webhook handler (if you passed webhookURL at session creation) ──
app.post('/api/pulseproof-webhook', express.json(), (req, res) => {
const { sessionID, token, trustTier, expiresAt, trustScore, verifiedAt } = req.body;
// token arrives here automatically when iOS app completes
// No polling needed — save directly
console.log(`Verified: tier=${trustTier}, score=${trustScore}`);
res.json({ ok: true });
});
# No API key needed — decode the CBOR token directly
# pip install cbor2
import base64, time, cbor2
from flask import request, jsonify
@app.route('/api/verify-human', methods=['POST'])
def verify_human():
token = request.json.get('token')
if not token:
return jsonify(ok=False, reason='missing token'), 400
# Decode the CBOR proof token
raw = base64.urlsafe_b64decode(token + '==')
payload = cbor2.loads(raw)
# Check expiry
if payload['expiresAt'] < time.time():
return jsonify(ok=False, reason='token expired'), 401
# trustTier: 1=camera, 2=watch+camera, 3=zkSovereign
trust_tier = payload['trustTier']
# Mark user verified in your DB
db.users.update(verified=True, trust_tier=trust_tier)
return jsonify(ok=True, trustTier=trust_tier)
# Webhook handler (if you passed webhookURL at session creation)
@app.route('/api/pulseproof-webhook', methods=['POST'])
def pulseproof_webhook():
data = request.json
# token arrives here automatically when iOS app completes
print(f"Verified: tier={data['trustTier']}, score={data['trustScore']}")
return jsonify(ok=True)
Create a verification session. Call this from your frontend before redirecting the user. No API key required — the relay is open.
HTTP// Request
POST https://verify.pulseproof.app/api/sessions
Content-Type: application/json
{
"appID": "com.yourcompany.yourapp", // shown in iOS consent screen
"callback": "https://yourapp.com/verified",
"ttl": 600, // optional, max 600s (10 min)
"webhookURL": "https://yourapp.com/api/pulseproof-webhook" // optional
}
// Response 200
{
"sessionID": "a1b2c3d4-...",
"verifyURL": "https://verify.pulseproof.app/start?session=a1b2c3d4&callback=..."
}
Poll for the verification result. The session persists until its TTL expires and supports multiple reads. Returns the full CBOR token when complete.
HTTP// Response (pending)
{ "status": "pending" }
// Response (complete) — session persists until TTL, multiple reads OK
{
"status": "complete",
"token": "hqFhd...", // CBOR proof token, base64url encoded
"trustTier": 3, // 1=camera, 2=watchFused, 3=zkSovereign
"expiresAt": 1743382800, // Unix epoch
"trustScore": 87 // 0-100
}
// Response (expired/not found)
404 Not Found
Every field returned by the relay. trustTier is also delivered directly in the callback URL params — no polling needed for most integrations.