Error reference
Every error type and failure reason the SenseCrypt Mobile SDK can produce, what causes it, and whether it is recoverable.
The SDK has four error surfaces:
| Surface | Type | Where it appears |
|---|---|---|
| Opening the SDK | AuthnInitError | Thrown by open(). |
| Loading models | SenseCryptInitError / SenseCryptInitException | Thrown by initializeInferenceEngine(). |
| Starting a flow | AuthnRuntimeError | Thrown by startRegistrationFlow, startRevocationFlow, startKeyRotationFlow. |
| Inside a flow | FailureReason | Carried on failed and postingResultFailed states — not thrown. |
The fourth is the one that matters most in practice: flows don't throw once started, they emit terminal states.
Initialization errors
AuthnInitError — from open()
| Case | Cause | Recoverable? |
|---|---|---|
InvalidPortalUrl(url, details) | portalUrl won't parse. Usually a trailing slash or missing scheme. | Fix the config. Your bug. |
SecureHardwareUnavailable | No usable secure key store on the device. | No. The SDK cannot operate here. Show a terminal message. |
BiometricNotEnrolled | The device has no biometric enrolled. | Yes — send the user to system settings, then retry open(). |
Internal(details) | Unexpected core failure. | No. Report it. |
SenseCryptInitError — from initializeInferenceEngine()
| Case | Cause | What to show |
|---|---|---|
LicenseNotFound(path) | No mobile.lic in the bundle or assets. | Nothing — a packaging bug. Fix the project. |
LicenseInvalid(reason) | Present but failed parse or signature check. | Terminal error. |
LicenseExpired | Valid but past expiry. | "Update your app." |
ModelLoad(reason) | A bundled model is missing or corrupt. | Terminal; damaged artifact. |
LivenessInit(reason) | Liveness pipeline data missing or corrupt. | Terminal; damaged artifact. |
LicenseExpired is the only one of these five that is not a bug on your side. A current release ships a fresh license, so an app update is the fix — route users to the store rather than to support.
Runtime errors
AuthnRuntimeError — from flow starters
| Case | Cause | Handling |
|---|---|---|
NoKeys | startKeyRotationFlow() with nothing to rotate. | Hide the rotate action when keys is empty — but still catch this, because the last key can be revoked between your check and the tap. |
UnknownKey(keyId) | startRevocationFlow(keyId:) with an id this device doesn't hold. | Refresh your key list; it's stale. |
FlowFinished(flowId) | An operation was attempted on an already-terminated flow. | Start a new flow. |
Internal(details) | Unexpected core failure. | Report it. |
Failure reasons
FailureReason is carried on failed(reason:) — terminal — and on postingResultFailed(reason:), which is not terminal and can be retried with retryPostResult().
Network and server
| Reason | Payload | Meaning | Recoverable |
|---|---|---|---|
networkUnreachable | details | Couldn't reach the portal. | Yes — retry. |
portalError | details | The portal returned an error. | Sometimes; retry once. |
upgradeRequired | — | The server requires a newer SDK or app version. | No — prompt for an app update. |
Credential and session
| Reason | Payload | Meaning | Recoverable |
|---|---|---|---|
tokenExpired | — | The session behind the QR expired. | No — restart the sign-in. |
signatureRejected | — | The portal rejected the device signature. | No. Suggests key or clock trouble; may need re-enrollment. |
clockSkew | — | The device clock is too far off for signed requests to validate. | Yes — tell the user to enable automatic date and time. |
accessDenied | — | The identity is not permitted to complete this sign-in. | No. |
Face and liveness
| Reason | Payload | Meaning | Recoverable |
|---|---|---|---|
faceMismatchMaxRetries | — | The mismatch budget was exhausted. The SDK has already signed an abort to the portal. | No — restart the sign-in. |
livenessFailed | — | Liveness could not be established. | No at the flow level. Within a capture, liveness retries loop back without consuming budget. |
User and device
| Reason | Payload | Meaning | Recoverable |
|---|---|---|---|
biometricDenied | — | The device biometric prompt was refused or failed. | Yes — retry. |
userDeclined | — | The user declined the request. | Terminal by intent. |
loginCancelledByUser | — | Terminal state after cancelLogin(). | Terminal by intent. |
deviceKeyRevoked | email, clientId, appLabel | This device's key was revoked server-side. | No — re-enrollment required. Name the app and email from the payload. |
deviceKeyTerminated | email, clientId, appLabel | The key was terminated server-side (a stronger revocation). | No — re-enrollment required. |
Registration and signup
| Reason | Payload | Meaning | Recoverable |
|---|---|---|---|
pinAttemptsExhausted | — | Too many wrong PINs. | No — restart registration. |
pinExpired | — | The PIN aged out. | No — restart, or resend before expiry next time. |
emailAlreadyExists | — | Signup for an address that already has an identity. | No — route to sign-in instead. |
signupNotAllowed | — | The application forbids self-signup. | No — direct the user to an administrator. |
Internal
| Reason | Payload | Meaning |
|---|---|---|
cryptoFailure | details | A cryptographic operation failed. |
internal | details | Unexpected core failure. Also what you get from calling retryPostResult() in a state with no cached inputs. |
In Swift, internal is a keyword — the case is spelled .`internal` with backticks.
Step-specific error enums
These are not failures. They are recoverable conditions carried inside non-terminal states, and the flow stays alive.
QrScanError — on awaitingQrScan
| Case | Payload | Meaning | Re-aiming helps? |
|---|---|---|---|
invalidQr | — | Not a SenseCrypt sign-in QR, or the portal returned a malformed face-token blob. | Yes — keep the scanner open. |
expired | — | The QR's expiry timestamp has passed (with a small grace window). It was a real code, it's just dead. | No — surface an "QR expired" message and send the user back. |
unknownKey | recipientEmail, appLabel, appLogoBytes, appLogoMime, appPrimaryColor, appAccentColor | The QR's (recipient, client_id) pair matches no key on this device. | No — offer "Add key for appLabel?" |
network | — | The lookup couldn't reach the portal. | Yes — retry. |
attestationFailed | — | Play Integrity (Android) / App Attest (iOS) refused to issue a token: the device failed the integrity verdict, your app is missing the required configuration, or attestation is unavailable (simulator). | No — terminal. Show a device-integrity message. |
accessDenied | — | The server's group access gate refused this (recipient, client_id) pair — the user was removed from the app's group, or the group was deleted or detached. | No — terminal. Show "contact your administrator". |
invalidQr and network are worth keeping the scanner alive for; call resetToScan() to clear the error and resume. The other four cannot be fixed by re-aiming the camera — treat them as end-of-scan and route the user somewhere useful.
unknownKey carries the relying party's branding — logo bytes, MIME type, and colours — precisely so your "add a key?" prompt can render the app's identity rather than a bare client_id. Fall back to an org-initial monogram when appLogoBytes is nil.
attestationFailed fires on the simulator and emulator, where hardware attestation is unavailable. Expect it during local development; it does not mean your integration is broken.
PasswordError — on downloadingFaceToken
| Case | Meaning |
|---|---|
wrongPassword | Rejected; re-prompt. |
network | Verification couldn't reach the portal. |
FaceCaptureRetryReason — on awaitingFaceCapture / awaitingCapture
| Case | Meaning | Counts against budget? |
|---|---|---|
faceMismatch | The face didn't match the enrolled token. | Yes. |
livenessFailed | Liveness wasn't established. | No. |
Handling patterns
Distinguish recoverable posts from hard failures. postingResultFailed means the proof is cached and one retryPostResult() away from succeeding — no re-scan, no re-capture. failed means start over. Presenting both as "something went wrong, try again" makes users redo work they don't need to.
Don't build your own retry counter for face mismatches. The budget lives in attemptsUsed / attemptsMax on the capture state, and the SDK signs the abort when it's exhausted. Render the numbers; don't reimplement the logic.
Treat deviceKeyRevoked and deviceKeyTerminated as re-enrollment triggers. Both carry the email and appLabel, so you can say exactly which enrollment is gone rather than showing a generic error.
clockSkew has a real user fix. It's the one cryptographic failure with an obvious remedy — automatic date and time. Say so explicitly instead of showing a generic signing error.
Related
- Initialization — where the init errors surface.
- Login — the retry and cancel semantics in context.
- Reference — the full type reference.