Mobile
Two shapes. Your own iOS app links the Swift SDK. A browser-only product hands off to the Root Herald companion app, which posts evidence to the backend you registered.
import RootHerald
let client = RootHeraldClient()
// Your backend minted the challenge with ask: ["identity"] — iOS has no posture to prove.
let evidence = try await client.respond(to: challenge)
try await api.verify(challengeId: challengeId, evidence: evidence.jsonData()) // → rh.verify() on your backendThe full surface — enrollment, assert for a key verdict, the errors — is on the iOS SDK page.
Browser-only products: the mobile bridge
App Attest is reachable only from a native app. Register two https URLs on your backend under Dashboard → Mobile attestation: an app-verify URL the bridge forwards evidence to, and a return URL the app reopens with ?rhcid=<challengeId>. The app only ever posts to Root Herald and Root Herald only ever forwards to the URL you registered, so a page cannot redirect the evidence.
import { buildMobileAttestLink } from "@rootherald/contracts";
const { challengeId, challenge } = await post("/api/rootherald/challenge", {}); // ask: ["identity"]
anchor.href = buildMobileAttestLink({
bridgeBaseUrl: "https://bridge.rootherald.io",
tenant: "acme", // your public tenant handle
challengeId,
challenge, // relayed verbatim; the app reads the ask from it
});
// Render a real <a> the user taps. Universal Links do not fire on location.href or a redirect.import { RootHeraldClient } from "@rootherald/node";
const rh = new RootHeraldClient({ secretKey: process.env.RH_SECRET_KEY! });
export async function POST(req: Request) {
const body = await req.json(); // { challengeId, evidence: { iosAttestation } }
const result = await rh.verifyMobileEvidence(body); // the metered verify, with your key
await store.put(body.challengeId, result); // the return page polls this
return Response.json({ ok: true }); // 2xx tells the bridge to hand the user back
}const challengeId = new URLSearchParams(location.search).get("rhcid");
const result = await poll("/api/rootherald/result?challengeId=" + challengeId);
if (result.assuranceClaimsMet?.includes("rootherald:assurance:real-device")) grantAccess();The companion app is in TestFlight preview and the bridge lands with its public release; the endpoints above do not resolve yet. verifyMobileEvidence is live in @rootherald/node. Use a mobile-permissive policy: iOS resolves to urn:rootherald:device:any, and a policy requiring device:high or an event log rejects every phone.