Skip to content
ClientsIn development

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.

your iOS app (Swift)swift
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 backend

The 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.

your mobile web pagets
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.
your backend — the registered app-verify URLts
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
}
your return pagets
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();
Status

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.