Skip to content
Integrate · Mobile

Require Root Herald on mobile (browser-only apps)

App Attest — the iPhone equivalent of a TPM attestation — is only reachable from a native app, not a mobile browser. So a browser-only product can't collect it the way desktop does. The mobile bridgecloses that gap: your web page hands off to a Root Herald–hosted bridge that opens the Root Herald companion app, which posts App Attest evidence to your own backend — where you broker the same metered rh.verify() you already call on desktop.

You keep the key and the metering. The bridge only opens the app.

The companion app posts its evidence to one fixed Root Herald endpoint — never to a URL a page hands it — and the bridge forwards that evidence to the backend you registered. Your backend brokers the metered verify with your rh_sk_, exactly like desktop. Because the app never trusts a page-supplied address, a malicious page can't redirect the evidence. One verify call, two platforms.

1 · Register your two mobile URLs

In the dashboard under Mobile attestation, register two absolute https URLs on your own backend. Root Herald will only ever open the app pointed at these — a page can never supply its own destination.

  • App-verify URL — where the bridge forwards the App Attest evidence, e.g. https://acme.com/api/rootherald/app-verify.
  • Return URL — the page the app reopens in Safari after posting, e.g. https://acme.com/rootherald/return. Root Herald appends ?rhcid=<challengeId> so this page knows which attestation to poll.

2 · Mint a challenge and open the app (your page)

On mobile, mint a challenge through your backend (same as desktop), then render the app-open link as a real link the user taps. The link carries only your tenant + the challenge — no URLs. iOS only opens an app on a genuine tap, never a redirect or scripted navigation.

your mobile web pagets
import { buildMobileAttestLink } from "@rootherald/contracts";

// 1. Your backend mints a challenge via Root Herald (holds your rh_sk_).
const { challengeId, nonce } = await fetch("/api/rootherald/challenge", { method: "POST" })
  .then((r) => r.json());

// 2. Build the app-open link (no URLs — the app posts evidence to Root Herald).
const link = buildMobileAttestLink({
  bridgeBaseUrl: "https://bridge.rootherald.io",
  tenant: "acme",            // your public tenant handle
  challengeId,
  nonce,
});

// 3. Render it as a real <a> the user taps. (A tap — not location.href.)
anchor.href = link;
It must be a tap.

Universal Links only fire on a real user tap. Setting window.location or issuing a redirect keeps the user in Safari and the app never opens.

3 · Receive the forwarded evidence and broker the verify (your backend)

The app posts its evidence to Root Herald; the bridge forwards { challengeId, evidence: { iosAttestation } } to your registered app-verify URL. Broker it with one call, then store the verdict keyed by challengeIdso the returning page can read it. (Return a 2xx — that's how the bridge knows the app can hand the user back.)

your backend — POST /api/rootherald/app-verify (called by the bridge)ts
import { RootHerald } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! });

export async function POST(req) {
  const body = await req.json();                 // { challengeId, evidence: { iosAttestation } }
  const result = await rh.verifyMobileEvidence(body);   // brokers the metered verify with your key
  await store.put(body.challengeId, result);     // keep it for the return page to poll
  return Response.json({ ok: true });
}

4 · Read the verdict on the return page

After posting, the app reopens your return URL with ?rhcid=<challengeId>. Poll your own result endpoint for the stored verdict and continue — allow, deny, or step up — exactly as you would on desktop.

your return pagets
const challengeId = new URLSearchParams(location.search).get("rhcid");
const verdict = await pollResult("/api/rootherald/result?challengeId=" + challengeId);
if (verdict.assuranceClaimsMet?.includes("real-device")) grantAccess();
What mobile proves — and what it doesn't.

App Attest proves a genuine, distinct Apple device with a Secure Enclave — so the real-device claim and Sybil-resistance work the same as desktop. Boot-integrity claims (Secure Boot, measured boot, OEM-keyed) are desktop-TPM only; on iOS those policy checks report not-applicable rather than failing. See Verdict, field by field for the platform-by-platform matrix.