getSession() when the user performs a sensitive action, and POST the sealed token to your backend for verification.
Requirements
- iOS 14+ for the public binary SwiftPM package
- Swift 5.9+
- Xcode 15+
- A Foil publishable key, starting with
pk_live_orpk_test_
Install
Add Foil with Swift Package Manager:Foil product to your app target. In Xcode, choose
File > Add Package Dependencies…, enter
https://github.com/abxy-labs/foil-ios, choose version 1.1.0 or newer,
and add the Foil product to the target that configures the SDK.
Configure on startup
Configure once at app launch — typically from yourApp initializer — so signal collection is running by the time the first screen appears.
FoilConfiguration
| Field | Default | Description |
|---|---|---|
publishableKey | — | Required. Must start with pk_live_ or pk_test_. Secret keys (sk_*) are rejected at configure-time. |
enableBehavioralSignals | true | Zero-config native behavioral capture: app lifecycle, navigation, input, scroll, and motion. Touch-stroke observers are opt-in. |
enableHiddenWebView | true | Enables the attach(to:) handoff so an in-app WKWebView shares this session. |
enableCloudIdentifier | false | Opt-in iCloud KVS continuity hint. Requires the iCloud Key-Value Store entitlement on your app to survive reinstall. |
enableAutoAttachTouches | false | Automatically attaches touch observation to app windows. Leave off if you call observeTouches(on:contextId:) manually. |
apiEndpoint | production API | Advanced override for local development or private deployments. |
Get a session at action time
CallgetSession() from an async context right before the sensitive action — not on app launch.
SessionHandoff is a small value type:
sessionIdis stable for the life of the client.getSession()posts the native snapshot, performs a bounded best-effort behavioral flush, and returns the handoff your backend should verify.- The SDK never surfaces verdicts, scores, or visitor IDs to the device — verify on your server.
Behavioral capture
WhenenableBehavioralSignals is true, the SDK starts zero-config UIKit capture automatically. It records the native equivalents of the browser’s behavioral events: app lifecycle, hashed screen navigation, viewport and scroll, form focus and input, selection and clipboard, and motion.
No raw form text, raw placeholders, or raw field identifiers are sent. Field identity is hashed, and geometry is rounded to the field’s window coordinates so the dashboard can show timing and replay context without collecting user-entered values.
Touch-stroke dynamics are still opt-in per screen for higher-fidelity gesture data. Call observeTouches(on:) once the view is mounted.
UIViewRepresentable wrapper to expose the underlying UIView.
WebView correlation
If your app hosts Foil-protected web content in aWKWebView, attach it so the web SDK reuses the native session.
window.__FOIL_NATIVE__, which the browser SDK picks up when loaded inside the WebView.
Native identity and attestation
Native visitor continuity is resolved server-side from the SDK’s encrypted observations.install_id tracks the app install, device_id tracks the device continuity layer, and native visitor_id represents the same device within your organization. None of those identifiers are exposed to the app.
Apple App Attest is optional and positive-only by default. When App Attest is supported, Foil can use SDK-managed App Attest keys to strengthen native identity. When it is unsupported, unavailable, unconfigured, or fails verification, Foil falls back to the other native continuity signals unless your organization explicitly enables strict attestation.
To verify App Attest in production, add your iOS app identity in the Foil dashboard. Baseline native continuity still works without this setup, but App Attest will not upgrade identity confidence until the app identity is configured.
Diagnostics
dispatchHealth() returns per-channel counters of successful and failed batch posts since the last configure(_:). Use it to confirm signals are reaching the server when you don’t have server-side visibility — for example, to spot a TLS-pinning, proxy, or quota issue from the client.
consecutiveFailures rising without a matching rise in consecutiveSuccesses typically means a sustained network outage or a server-side rejection that retries can’t recover from. Counters reset on configure(_:), destroy(), or resetLocalState().
API reference
| Method | Description |
|---|---|
FoilClient.shared.configure(_:) | Validates the publishable key and starts the runtime. Throws FoilError on invalid input. |
FoilClient.shared.getSession() async throws -> SessionHandoff | Posts the snapshot, performs a bounded best-effort behavioral flush, and returns a sealed handoff. |
FoilClient.shared.waitForFingerprint() async throws | Resolves when the durable visitor fingerprint is ready. Optional — getSession() works without it. |
FoilClient.shared.observeTouches(on:contextId:) | Attaches a non-consuming gesture recognizer to the view. Call once per screen. |
FoilClient.shared.attach(to:) | Shares the current session with a hosted WKWebView. |
FoilClient.shared.dispatchHealth() async -> DispatchHealthSnapshot | Per-channel batch-post counters since the last configure(_:). Useful for confirming telemetry is reaching the server. |
FoilClient.shared.resetLocalState() | Stops the runtime and clears the durable session and outbox state. Long-lived install/device anchors are preserved. |
FoilClient.shared.destroy() | Stops timers and releases resources. Rarely needed outside tests. |
Error handling
FoilError is a Sendable struct carrying a stable code, a human-readable message, an optional underlying error, and a retryable flag.
| Code | Retryable | When it happens |
|---|---|---|
config.invalid_publishable_key | no | publishableKey is missing, uses the sk_* secret prefix, or doesn’t match pk_live_* / pk_test_*. |
client.not_configured | no | A client method was called before configure(_:). |
session.create_failed | varies | Session handshake with the Foil API failed. |
session.handshake_expired | no | The server expired the session. Call configure(_:) again to mint a new one. |
session.invalid_or_expired | no | The active session is invalid or expired. |
transport.batch_post_failed | varies | A signal batch upload failed. |
transport.rate_limited | yes | HTTP 429 from the API. Back off and retry. |
transport.upgrade_required | no | HTTP 410/426. The SDK version is too old — upgrade the app. |
transport.network | yes | DNS, TLS, or connectivity failure. |
crypto.failure | no | A crypto primitive failed. Usually a platform issue worth reporting. |
internal | no | Unexpected state. File a support ticket with the message and underlying error. |
code values are stable. Category prefixes (config.*, transport.*) are safe to branch on with a wildcard — new codes within a category keep the same retry semantics.
Fallback policy
If Foil fails, your app should still work. Log the error and continue without a handoff.Best practices
- Configure once at launch so signals are collecting before any screen opens.
- Call
getSession()late — right before the sensitive action, from an async context. - Degrade gracefully — never block the user if the SDK fails to produce a handoff.
- Reuse the shared client —
FoilClient.sharedis a process-wide singleton.
What’s next
Server verification
Verify the sealed token on your backend
Browser SDK
Equivalent guide for the web surface
Android SDK
Native Android integration
Going to production
Rollout checklist and monitoring