Chapter 01  /  iOS SDK

Integration
Guide.

iOS SDK v1.0

Swift 5.9+
iOS 16.0+
watchOS 9.0+ (optional)

5 steps from zero to verified.

01 / Install
Add the
Package

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
02 / Configure
Set Your
API Key

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
  }
}
03 / Verify
Present the
Verification UI

Call verify from any view controller. The SDK handles camera permissions, Watch connectivity, ZK proof generation, and Secure Enclave signing.

Swift
import 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)
      }
    }
  }
}
04 / Transmit
Send Token
to Backend

The verified token is a compact CBOR payload signed by the Secure Enclave. Post it to your server over HTTPS.

Swift
func 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()
}
05 / Backend
Verify on
Your Server

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 })
  }
)

Web
Integration.

Chapter 02  /  Webapp
7 steps · JS, Node, Python

Your Web App
PulseProof
Your Backend
Step 01
POST /api/sessions
Send appID, callback, ttl to relay
Returns {sessionID, verifyURL}
{sessionID, verifyURL}
Step 02
Save sessionID, open verifyURL
sessionStorage.setItem('pp_session', id)
window.location.href = verifyURL
window.location.href = verifyURL
Step 03
Safari → app opens directly
Universal Link triggers PulseProof app
Chrome → /start page loads, "Open PulseProof" button shown
Step 04
Biometric capture + ZK proof on-device
Apple Watch PPG or iPhone camera rPPG
Groth16 circuit · Secure Enclave ECDSA
CBOR token POSTed to relay
redirect to callback?session=…&trustTier=1
webhook POST
(if set)
Step 05
Callback loads with trustTier
?session=ID&status=success&trustTier=1
Read from URL params — no polling needed
Optional
Webhook received
{sessionID, token, trustTier,
expiresAt, trustScore, verifiedAt}
GET /api/sessions/:id (optional — for full CBOR token)
Step 06
User verified
trustTier in URL params (fast path)
or full CBOR token from relay (slow path)
Optional
Decode CBOR token
base64url-decode → cbor.decode()
Check expiresAt. No PulseProof call needed.
01
Web App
Create a session via POST /api/sessions
POST to the relay with your appID, callback URL, and optional ttl (max 600 seconds). The relay returns a sessionID and a ready-to-open verifyURL. No API key required.
02
Web App
Open the verifyURL
Use 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.
03
PulseProof
User taps "Open PulseProof" (Chrome) or app opens automatically (Safari)
The WebProofView consent screen shows your appID and asks the user to approve. Once approved, biometric capture begins.
04
PulseProof
Biometric capture + ZK proof, entirely on-device
Apple Watch PPG or iPhone front camera rPPG. A Groth16 circuit generates the proof. The Secure Enclave signs via ECDSA. The result is assembled as a CBOR token and POSTed to the relay. No raw biometric data ever leaves the device.
05
Web App
Callback URL loads with trustTier in params
The relay redirects the Chrome tab (or iOS opens the URL in Safari) to your callback: yourapp.com/callback?session=ID&status=success&trustTier=1. Read trustTier directly from URL params — no polling needed for most use cases.
06
Web App
Poll GET /api/sessions/:id for full CBOR token (optional)
If you need the cryptographic token for your backend, poll the relay. The session persists until its TTL expires (up to 10 minutes) and supports multiple reads. Returns {status, token, trustTier, expiresAt, trustScore}.
07
Backend
Decode CBOR token (optional)
base64url-decode the token string, then parse with 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)

API
Reference.

Chapter 03  /  REST API
POST /api/sessions · GET /api/sessions/:id

Endpoint 1
POST
/api/sessions

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=..."
}
Endpoint 2
GET
/api/sessions/:id

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
Field Reference
Response
Fields

Every field returned by the relay. trustTier is also delivered directly in the callback URL params — no polling needed for most integrations.

No API key required. The relay is open. Token authenticity is proven by the Groth16 ZK proof and Secure Enclave ECDSA signature inside the CBOR token. Your backend never needs to call back to PulseProof — just decode and verify the token yourself.
sessionID string UUID identifying this verification session. Pass to GET /api/sessions/:id to poll for the result.
verifyURL string Open this URL to start verification. Use window.location.href on iOS — triggers a Universal Link in Safari (app opens directly) and loads the /start page in Chrome (shows "Open PulseProof" button).
token string CBOR proof token, base64url encoded. Contains the ZK proof, trustTier, expiresAt, and Secure Enclave ECDSA signature. Decode with cbor (Node.js) or cbor2 (Python).
trustTier number 1 = Camera rPPG (iPhone front camera). 2 = Watch + Camera fused. 3 = zkSovereign (full Groth16 ZK proof). Also returned directly in the callback URL params — no polling needed.
expiresAt number Unix epoch seconds. The token is valid until this time. Check on your backend: payload.expiresAt < Date.now() / 1000.
trustScore number 0–100 confidence score from biometric signal quality analysis.
webhookURL string (request) Optional. If provided at session creation, the relay POSTs {sessionID, token, trustTier, expiresAt, trustScore, verifiedAt} to this URL when the iOS app completes. Preferred over polling for backend integrations.