Registration flow
Enroll a user and mint a device key with the SenseCrypt Mobile SDK — email PIN verification, the self-signup branch with attribute collection and consent, and face capture.
Registration proves control of an email address, then binds a device key to the identity. It has two branches that share a start:
- Existing identity — the user is already known to the directory. Verify the emailed PIN and a device key is minted. No face capture in this branch.
- Self-signup — the identity is new and the application allows signup. After the PIN, the SDK collects profile attributes and a consent acknowledgement, then captures a face to mint a fresh face token.
Which branch you land in is decided by the server, not by your app. Observe the state stream and render whatever it asks for.
Starting the flow
let flow = try authn.startRegistrationFlow(
email: "alice@example.com",
clientId: "your-client-id",
sessionId: nil
)
Task {
for await state in flow.stateStream() {
handle(state)
}
}val flow = withContext(Dispatchers.IO) {
authn.startRegistrationFlow(
email = "alice@example.com",
clientId = "your-client-id",
sessionId = null,
)
}
lifecycleScope.launch {
flow.state.collect { state -> handle(state) }
}| Parameter | Meaning |
|---|---|
email | The address to enroll. The server emails a PIN to it. |
clientId | Your application's client identifier. |
sessionId | The OAuth session id from a QR that initiated this registration — the "add a key" path, where a sign-in QR was scanned on a device with no key. Pass nil for registration your app initiated on its own. |
sessionId matters for the self-signup branch: when present, the SDK sends it on completion so the server finishes the OAuth session in the same call, and the user is not asked for a second face scan.
This method throws — see AuthnRuntimeError.
The state machine
States and their payloads
| State | Payload | What your UI does |
|---|---|---|
idle | — | Nothing yet. |
submittingEmail | — | Show a spinner. |
awaitingPin | email, attemptsLeft: UInt32, cooldownUntil: Int64?, pinExpiresInSeconds: UInt32?, codeDispatched: Bool | Show the PIN entry screen. |
verifyingPin | — | Spinner. |
collectingAttributes | email, attributes: [SignupAttribute], termsUrl: String?, privacyUrl: String?, rejected: SignupAttributeRejection?, priorValues: [String: String], priorAddress: [String: String]? | Render the signup form. |
awaitingFaceCapture | lastError: FaceCaptureRetryReason? | Open the camera. |
mintingFaceToken | — | On-device work; spinner. |
generatingDeviceKey | — | Spinner. |
completingRegistration | — | Spinner. |
postingResultFailed | reason: FailureReason | Not terminal. Offer "Try again" → retryPostResult(). |
succeeded | key: Key | Terminal. The minted device key. |
cancelled | — | Terminal. |
failed | reason: FailureReason | Terminal. |
Step 1 — the email PIN
flow.submitPin(pin: entered)
flow.resendPin()flow.submitPin(entered)
flow.resendPin()The awaitingPin payload gives you everything the screen needs:
attemptsLeft— remaining tries. Exhausting them terminates withfailed(reason: .pinAttemptsExhausted).pinExpiresInSeconds— for a countdown. Expiry terminates withfailed(reason: .pinExpired).cooldownUntil— Unix timestamp until whichresendPin()is rate-limited. Disable the resend button until then.codeDispatched— whether an email was actually sent on this transition.
Honour codeDispatched. It is false when the server suppressed the email — a PIN for this address was already requested inside the resend cooldown window, so the valid code is the one already in the inbox. It is also false on a wrong-PIN re-prompt. Showing "We sent you a code" unconditionally tells the user to look for an email that was never sent.
Step 2 — the self-signup branch
If the server flags signup, you get collectingAttributes with the directory's enabled attribute schema. Render one typed input per entry, in ordering order.
Each SignupAttribute carries:
| Field | Type | Meaning |
|---|---|---|
key | String | Identifier to key your submitted value by. |
dataType | AttributeDataType | Which input control to render. |
label | String | Display label. |
required | Bool | Whether it must be filled. |
ordering | Int32 | Form order. |
options | [String] | Allowed values for enumerated types. |
constraints | AttributeConstraints? | Additional validation constraints. |
name is included in the schema; email is not — it is already known.
The consent gate
termsUrl and privacyUrl are the effective Terms-of-Use and Privacy-Policy URLs, resolved server-side from a per-app override or the deployment default.
When these are non-nil, you must render an "I agree" checkbox linking both, and must not let the user advance until it is ticked. Pass the result as consentAccepted. The SDK echoes the URLs and the flag back to the server for the audit record.
nil means the server predates this feature; skip the consent gate in that case.
Submitting the form
flow.submitSignupAttributes(
values: ["name": "Alice Smith", "phone_number": "+15551234567"],
address: ["street_address": "1 Main St", "locality": "Springfield"],
consentAccepted: true
)flow.submitSignupAttributes(
mapOf("name" to "Alice Smith", "phone_number" to "+15551234567"),
mapOf("street_address" to "1 Main St", "locality" to "Springfield"),
true,
)values is keyed by attribute key, with scalars as strings. address carries the structured OIDC address sub-fields separately when that attribute was filled; pass nil if not. The SDK extracts name for its dedicated wire field and forwards the rest.
Handling rejection
The server validates values the device can't check locally — most commonly a coupon code. On rejection the flow re-enters collectingAttributes with:
rejected— aSignupAttributeRejection. Itskeynames the offending field, or isnilfor a form-level error such as a network blip;messageis the server's reason. Show it inline and re-enable the form.priorValues/priorAddress— what the user already typed. Re-populate the form from these so nothing has to be retyped. Both are empty/nilon first entry.
Step 3 — face capture (self-signup only)
Same driver pattern as login:
let driver = flow.faceCaptureDriver()
SenseCryptFaceScanView(driver: driver) { outcome in
// CaptureOutcome
}val driver = flow.faceCaptureDriver()
// Observe driver.state and submit via driver.submitFaceCapture(handle).Signup has no face-mismatch retries. There is no existing token to match against — the SDK is minting a fresh one — so the only retry reason here is livenessFailed. faceMismatch never occurs in this branch, and the corresponding attemptsMax on the capture state is 0, meaning no mismatch budget is enforced.
Step 4 — completion
On success you get succeeded(key:) carrying the minted Key. The device can now approve sign-ins.
postingResultFailed(reason:) works exactly as in login: not terminal, call retryPostResult() to re-send the cached signup proof. No face re-capture, no device-key re-mint. Gate the retry CTA on observing that state.
flow.retryPostResult()// Retry the signup completion through the capture driver.
driver.retry()Android's RegistrationFlow wrapper does not expose retryPostResult() — route the retry through the FaceCaptureDriver, whose retry() re-fires the same cached POST.
Registration-specific failure reasons
Beyond the common reasons, these appear mainly here:
| Reason | Meaning |
|---|---|
pinAttemptsExhausted | Too many wrong PINs. |
pinExpired | The PIN aged out before submission. |
emailAlreadyExists | Signup attempted for an address that already has an identity. |
signupNotAllowed | The application does not permit self-signup. Direct the user to an admin. |
livenessFailed | Liveness could not be established. |
Other enrollment paths
Registration through the SDK is one of three ways a user gets enrolled. The other two happen outside your app:
- Admin provisioning — an operator creates the user in the console, optionally from a photo.
- Directory provisioning (SCIM) — your IdP creates a pending shell that the user claims on first use. See SCIM.
In every path, control of the email address is proven before a device key is bound.
Related
- Login — approving sign-ins once enrolled.
- Key management — rotating and revoking device keys.
- Errors — the complete failure surface.
- Reference —
Key,SignupAttribute, and the enums.
Login flow
Approve a sign-in with the SenseCrypt Mobile SDK — scan the QR, handle an optional password step, capture a face, and post the result.
Key management
Rotate and revoke device keys with the SenseCrypt Mobile SDK — the make-before-break rotation ceremony, the rotation hint, and single-key revocation.