SenseCrypt Docs

Changelog

Release notes for SenseCrypt — how the product is versioned and where notable changes are recorded.

Notable changes to SenseCrypt are recorded here.

How we version

SenseCrypt exposes a versioned HTTP API — the endpoint paths are prefixed (for example /v1/...), and the current surface is described in the API reference. Breaking changes to a released API version are avoided; where behavior needs to change incompatibly, it is introduced under a new version. Entries are grouped by release, newest first. Unreleased holds what is merged and awaiting a release — when that release is cut, the heading is renamed to its version and dated, so a dated heading is always something you can run. Earlier releases collects the entries that predate those version headings.

Unreleased

3.7.0 — 29 October 2026

Mobile SDK: a lost completion answer is re-sent once; iOS streams deliver the current state once

A /register/complete or /signup/complete whose answer is lost at the transport level (no answer, a reset, a read timeout) is now re-sent once by the Mobile SDK itself, after a short pause, before postingResultFailed is reported; the server answers the re-send with the original response and writes nothing, so nothing is enrolled twice. A 5xx or a 429 still parks for your Try again at once, and a refusal is unchanged. On iOS, every flow's stateStream() and the capture driver's captureStateStream() now deliver the current state exactly once before the transitions; they used to deliver it twice, so a collector that skipped one replayed value was wrong by one. Android is unchanged. The SenseCrypt Authenticator for iOS (1.6.0, build 120) carries both: Continue on the one-device-per-user confirmation works on the first tap and shows its loader, and an enrolment the server completed no longer ends on "Couldn't add this device" when only the answer was lost. See registration.

A suspended user who tries to add a phone is told so

A suspended user's address used to look new to enrolment: on an application that allows self sign-up the SenseCrypt Authenticator ran the whole sign-up, face scan included, and ended on "An account already exists for this email"; on one that does not, no code ever arrived. Now /register/start answers a suspended address exactly as an active one and mails the code, and the correct code at /register/verify is answered 403 account_suspended, as is a CIBA sign-in link at /ciba/signup-start. The code is consumed by the refusal, a wrong code is answered as for anyone, and nothing is said to a caller who does not hold the mailbox. The Mobile SDK reports it as failed(.accountSuspended), a terminal reason with no retry and no self sign-up; an older SDK shows a generic failure. The SenseCrypt Authenticator shows "This account has been suspended. Contact your organisation's administrator." Reinstating the user lifts it. A sign-in page opened while the user was suspended used to answer a passkey with "that passkey belongs to a different account" even after the reinstate, until a new sign-in was started; it now completes. See the error codes.

One device per user; device protocol 12

A tenant can now allow each user one device: on the Issuer page under Devices, turn on Allow one device per user (PATCH /v1/admin/issuer with one_device_per_user: true; GET reads it back, and the change is recorded as tenant.updated). Off by default, so existing tenants are unchanged. When it is on, adding the SenseCrypt app on a further phone signs the user out of their other phones: the new phone shows which device will go, by the name and last use its own enrolment reported, and nothing changes until the user confirms. Confirming revokes the other phones' keys in the same transaction as the new one, and the passkey on them stops working; the user's other accounts on those phones, and their browser sessions, are not affected. The old phone learns on its next request, on the app's next sync, or at its next key rotation, and the SenseCrypt Authenticator shows Account moved to another device instead of offering to add the account again. Turning the setting on signs nobody out by itself; it applies from the next time a user adds a device. A reinstalled phone counts as a further device: the user is asked to sign out their earlier copy. The email the user already receives when a device is added says when their other device was signed out, so a user who did not do it knows to contact support or to add their own phone again, which moves the account back.

Self-hosted operators: on the first daily device-key sweep after this release, device records that earlier clean-ups had left without any key are removed as well. That is a one-off drop in the device count, not an incident.

Mobile SDK: device protocol 12 is additive and not a forced update; the oldest supported version stays 10. A protocol-12 app names its phone on /register/verify and reads replaces_devices from the answer; the registration flow then pauses in awaitingDeviceReplacementConfirm(otherDevices:) until confirmDeviceReplacement() or cancel(). A completion that would sign another phone out without the confirmation answers 409 device_replacement_required with the same list and leaves the registration token usable; the flow holds the request and asks. A key revoked this way carries cause: "device_replaced" on the device_key_revoked answer, the branding sync and /rotate/start, which the SDK surfaces as deviceKeyReplaced, RevokedKeyNotice.replaced and BrandingSyncSummary.replacedKeys. An app on protocol 10 or 11 keeps working everywhere except that, under a tenant with the setting on, adding a second phone answers 426 upgrade_required and the app shows its update sheet until it is updated. See registration and the error codes.

Signing out with an ID token from another session asks first

/v1/idp/oidc/logout used to end the browser's SenseCrypt session on any valid ID token for the same user, even one issued in a different browser session. Because an expired ID token is accepted there, an old token could sign the user out of every browser without a prompt. Now the browser's session ends without asking only when the ID token came from that session: its sid names it, or, for a token without sid (an application with no back-channel logout URL), your application signed in through that session. Otherwise SenseCrypt still ends the session the token names and revokes your application's refresh tokens, then asks the user before it ends the browser's session, and does not redirect to your post_logout_redirect_uri. Keep the ID token per sign-in session, not per user, and send the one from the current session. See Log out.

In-app sign-in says when the phone's clock is wrong

A phone whose clock is wrong is already told so at the start of an in-app sign-in. If the clock jumped back by more than the allowed window (a minute by default) between the face scan and the completion, the completion was refused as the generic invalid_session, the same answer as a wrong session or device, so the Mobile SDK could only report a server error. That completion now answers 400 invalid_request with kind: "clock_skew", the signal the device plane already sends, and the SDK reports server("device clock is out of sync"): tell the user to turn on automatic date and time. Every other refusal of the device signature is unchanged and still invalid_session, so a caller without a valid signature learns nothing new. Every Mobile SDK that offers in-app sign-in already understands the signal; no app update is needed.

Administrators can suspend and reinstate a user

The user detail in the console's Auth Users page now has a Suspend button and, for a suspended user, Reinstate. Suspending stops the user signing in to every application, and revokes every device, passkey, refresh token and browser session they hold at once; applications that opted into the session-revoked back-channel logout initiator are told. It is the same suspend a SCIM active: false performs. Reinstating lets the user sign in again with the same sub, attributes, roles and groups. Revoked credentials stay revoked, so the user adds their phone again with a code sent to their email and a face check. A user provisioned by SCIM can't be suspended from the console; suspend them in your identity provider. Both actions need Edit users. On the Management API, POST /v1/admin/auth_users/{auth_user_id}/suspend and POST /v1/admin/auth_users/{auth_user_id}/reinstate return the user, recorded in the audit log as auth_user.suspended and auth_user.reinstated, and every user the API returns now carries active. See Suspend or reinstate a user.

3.6.0 — 22 October 2026

A new account joins a phone only with proof it runs on that phone; device protocol 11

A phone's device_id is chosen by the app, and enrolment used to attach a new account's key to whatever device record the request named, in any tenant. Another identity that learned a phone's id could join it, rotate that phone's keys and move its push notifications. Enrolment now decides who may join a phone's record. On an iPhone, a further account must present the phone's App Attest key id, which the SenseCrypt Authenticator and the Mobile SDK already send. Where there is nothing to check against (Android on the current app), the account is admitted but cannot rotate the other accounts' keys, replace or clear the phone's push token, or vouch for another account until the app's next background sync confirms it belongs to the phone. A rotation refused in that window answers 403 with code: "rotation_row_membership" and changes nothing; retry after the sync. /v1/device/keys/rotate/start now names an email only for the signing account.

Mobile SDK: device protocol 11 is additive and not a forced update; the oldest supported version stays 10. A protocol-11 app signs /register/complete and /signup/complete with a device key it already holds. Once a phone has made a signed call at protocol 11, a new account can join it only with that signature, and an unsigned attempt gets 401 signature_required with the registration token left usable. See the error codes.

Administrators can revoke one of a user's devices

The user detail in the console's Auth Users page now has a Devices tab listing the phones the user has added, with each phone's name and OS, and when it was added and last used. Revoke stops that one phone signing in or approving requests for the user, and the passkey on it stops working; the user's other phones and other accounts on the same phone are not affected. The phone learns on its next request: the SenseCrypt Authenticator removes the account and offers to add it again, and a Mobile SDK app receives deviceKeyRevoked. The user can enroll the phone again; to stop them signing in, delete or suspend the user. Listing needs View users and revoking needs Edit users. On the Management API, GET /v1/admin/auth_users/{auth_user_id}/device_keys lists a user's devices and DELETE /v1/admin/auth_users/{auth_user_id}/device_keys/{key_id} revokes one, recorded in the audit log as auth_user.device_key_revoked. Phones added before this release show as "Unknown device" until the user adds them again. See Revoke a user's device.

Choose how users are alerted about a backchannel request

Each application now chooses how SenseCrypt alerts the user about a new CIBA request or face-PKI ceremony: app push, email, both (the default, so existing applications are unchanged) or neither. With neither, SenseCrypt posts each request to the application's user_alert_endpoint and the application alerts the user itself. Such an application must send client_notification_token and login_hint on every request. Whatever is chosen, a pending request also waits on the Authenticator app's home screen until it expires. Set it on the application's Tokens & Security tab, under How the user is alerted. See Alerting the user.

Console sign-in and forgot-password take the same time for every address

The admin console's sign-in and Forgot Password already answered every address with the same message. They now also do the same work before answering: a refused sign-in costs one password comparison whether or not the address has an account or a password, the reset email is sent after the response has been returned instead of before, and a tenant-host request hashes a throwaway password when no administrator matches. Response time therefore says nothing about which addresses exist or how they sign in. Messages and status codes are unchanged.

Delegated administrators adding a role

An administrator can add a role to another administrator only when their own access covers that administrator's, in every tenant they belong to: the rule that already applied to replacing or removing their roles. This applies to POST /v1/admin/administration/{member_id}/memberships, and to an invite sent to the address of an existing administrator, which adds roles to them. So a delegated administrator can no longer add an existing administrator to a tenant by invite, even to their own tenant, unless they hold Invite administrators and their access covers that administrator's in every tenant that administrator belongs to. The account owner is not limited. A refused request returns 403 privilege_ceiling and changes nothing.

A CIBA token hint is honoured only for the application it was issued to

/bc-authorize accepted, as id_token_hint or login_hint_token, any ID token this tenant had issued, so an application could start a sign-in for the user named in an ID token minted for another application in the same tenant. The token's aud (and azp, if it has one) must now name the requesting application, the same rule /authorize applies. An application that sends another application's ID token now gets unknown_user_id, the same answer as a hint that does not verify, and no sign-in email is sent. Send your application's own ID token, or the user's email as login_hint. See CIBA.

The Administrators page says why a request was refused

When the console refused an invite, a role change, a password reset or a removal on the Administrators page, it showed only a generic sentence such as "Could not invite administrator". It now says what went wrong: for example, that the address already belongs to an administrator you cannot manage, or that you lack the capabilities for the change, with those capabilities listed as on the Roles page. Inviting your own address is now refused in the form. The Activity page likewise says why it could not load. The API is unchanged.

3.5.1 — 16 October 2026

Submitting the Set your password page with a link that had expired, had already been used, or belonged to an address that became a provider account in the meantime showed only "The request was invalid". The page now shows the reason: "Invalid or expired link", or "Email already verified, please log in" when the account is already set up. On the API, POST /v1/admin/auth/verify-email and POST /v1/admin/auth/reset-password answer these cases with the management envelope — 400 {"error": "link_invalid"} for a dead link on either route (and on the administrator invite's POST /v1/admin/administration/auth/set-password) and 400 {"error": "email_already_verified"} for a spent verification link — instead of the OAuth-style invalid_request body. See Console sign-in methods.

3.5.0 — 15 October 2026

Mobile SDK: every shipped flow resumes after a lost answer

A completion that gets no answer, a 5xx or a 429 now parks in postingResultFailed on every flow — including the device-add /register/complete, which used to end the flow — and retryPostResult() re-sends the same request. Any other refusal is the server's verdict on that request and ends in failed, so a retry can no longer loop on it. A passkey face-token fetch that gets no answer is asked again inside its wait; a /register/start or CIBA start that gets no answer returns the flow to the state it left, so resendPin() on the same flow asks again; and a push token that rode a request the server did not take is kept for the next one. Android's RegistrationFlow wrapper gains retryPostResult(). See registration and login.

Attestation under enforce now applies to enrolment; device protocol 10 is a forced update, breaking for Mobile SDK apps

A tenant on its own app that moved attestation to enforce could still enrol devices from an app that would never pass the sign-in check — the stock SenseCrypt Authenticator, a build with the wrong package or bundle, a simulator — because enrolment only verified attestation material that was present, and an iOS install that already held an App Attest key was checked on the key id alone. The key was minted and failed at its first sign-in, and it could not be moved to the right app. Behaviour change: under enforce, /register/complete and /signup/complete now apply the same identity check as sign-in and refuse such an enrolment with the uniform attestation_failed, absent material included. On Android that check reads a Play Integrity token the Mobile SDK sends with the completion. On a deployment that requires attestation, the same rule now applies to the SenseCrypt Authenticator: an enrolment that carries no attestation is refused. Forced update, breaking for Mobile SDK integrators: the device protocol moves to version 10 and the oldest supported version rises to 10. Every app built on an older Mobile SDK — the SenseCrypt Authenticator and any app you build on the SDK yourself — gets "Update required" on every device request until it is rebuilt on the protocol-10 SDK; keys it already holds keep working after the update. From 15 October 2026 an app built on an older SDK shows "Update required" until you ship it rebuilt on the protocol-10 SDK. The protocol-10 SenseCrypt Authenticator is published on Google Play and the App Store; its users update it from the store. Every attestation check, at enrolment or on a sign-in, now records its verdict on the device key, GET /v1/admin/mobile-app reports the tenant's live keys as attestation_readiness (passing, failing, never checked), the console's Mobile App page shows the same under the attestation mode, and a change that would activate enforce while any key is failing or never checked answers 409 mobile_app_enforce_confirmation_required until it carries confirm_enforce: true. Keys enrolled under off or audit are not re-checked retroactively; under enforce they are refused at their next sign-in, and switching back to audit restores them. Each result is recorded against the app identity it was checked against, so after a switch from the SenseCrypt Authenticator or an identity change, earlier results count as never checked, and changing the identity while on enforce asks for the same confirmation. On a deployment that enforces attestation for the SenseCrypt Authenticator, Android enrolment with a Mobile SDK that sends the new token also depends on Google's Play Integrity API being reachable, as sign-in already does. See Ship your own Authenticator app, Running as the tenant's app and the error codes.

The SenseCrypt eID documentation moved to /eid-docs

The eID documentation is now at sensecrypt.com/eid-docs. Every link to the old address, /sensecrypt-eid-docs, redirects permanently to the same page at the new one.

KMS signing keys: rotation overlaps are billed

KMS-held signing keys are billed at the per-key monthly fee on the highest number active at the same time in each period. The count is now taken whenever an administrator rotates, stages, activates, reactivates or discards a signing key, not only when someone opens a usage or billing view. So a key switched on and off within one period is billed, and so is the overlap of a rotation: an OIDC rotation with a grace period, or a SAML key staged next to the current one, normally bills one extra key-month for each key that overlaps. SAML activation defaults to a 7-day grace period. An immediate OIDC rotation, the default, bills no extra key-month. Hosted service only.

Delegated administrators and shared roles

An administrator can change a role's capabilities only when their own access covers that of everyone the change reaches: every administrator who holds a console role, in every tenant they belong to, or every M2M app bound to an M2M role. A rename or a description edit does not need this check. The account owner is not limited. A refused change returns 403 privilege_ceiling and changes nothing.

PKCE error names the setting

When an application requires PKCE and a client sends no code_challenge, the error_description now says which setting to change: a confidential client that cannot send one, such as an Auth0 or Stytch connection, needs Require PKCE turned off in the application's Tokens & Security settings. SPA and FAPI applications always require PKCE.

3.4.0 — 8 October 2026

Mobile SDK: in-app sign-in, face PKI during sign-in, and session refresh

The Mobile SDK gains SignInFlow: a tenant's own app signs the user in with a face scan inside the app, with no browser and no QR, and holds IdP-issued, DPoP-bound tokens. One startSignInFlow call drives the whole exchange; the app observes a state machine and attaches the tokens to its own API calls through an OkHttp interceptor or a URLSession helper. A sign-in can carry a face-PKI operation (public keys, a signature or a shared secret) whose result is returned to the app, and a sign-in with offline_access is refreshed without a face scan until the IdP ends it. A confidential client supplies its client assertions through a ClientAssertionProvider. See In-app sign-in.

Accept DPoP-bound CIBA tokens on a FAPI 2.0/CIBA application

A FAPI application with a CIBA delivery mode binds its tokens to the client certificate, the configuration every certified FAPI-CIBA profile runs. An application can now accept DPoP-bound CIBA tokens instead (ciba_dpop_binding_allowed, default off, needs dpop_bound_access_tokens on; console: Tokens & Security → Backchannel authentication). The CIBA and refresh_token grants then bind with DPoP exactly as the authorization code grant does. The application stays sender-constrained under FAPI 2.0 and stands outside the certified FAPI-CIBA profiles, which the console says on that application. Where both the certificate binding and the acceptance are registered, the certificate still wins. For a DPoP-bound client, the authorization_pending and slow_down answers carry the next DPoP-Nonce. See FAPI 2.0/CIBA clients and the error codes.

Face PKI: tenant-scoped keys for allowed applications

Face-PKI keys stay specific to the application that asks, by default. An admin can now allow an application to use tenant keys, which every allowed application in the tenant shares: set face_pki_tenant_scope_allowed on the application, or turn on Allow tenant-wide face PKI keys in the console. A ceremony asks for them with the new bc-authorize parameter face_pki_scope=tenant; app is the default, and existing keys do not change. A tenant request from an application that is not allowed it, or any other value, is refused invalid_request before any email or push. The /payload ceremony data and the ID token's face_pki receipt gain scope. A first-party in-app sign-in session tells the SDK whether the application may use tenant keys. The signed device protocol moves to version 9; the floor stays at 7, and only a tenant-scope ceremony needs 9. The Management API gains the rule code face_pki.tenant_scope_requires_face_pki. If an admin turns the allowance off while a tenant-scope ceremony is pending, the phone's next step ends the ceremony. The phone and the application then get access_denied. See Key scope, the Face-PKI ceremony flow and the error codes.

3.3.0 — 6 October 2026

In-app sign-in for a tenant's own mobile app

A tenant that runs its own mobile app can let that app sign a user in without a browser redirect: a tap, a face scan in the app, and IdP-issued DPoP-bound tokens. It is off by default and opt-in per application (first_party_challenge_enabled; console: Tokens & Security, Allow in-app sign-in from your mobile app), needs DPoP-bound access tokens without a client-certificate binding, and is refused on the stock Authenticator. The endpoint behind it is not a public surface: it is absent from discovery and from the API reference, and the Mobile SDK drives it. The start answers every email identically, and a hint that cannot sign in gets a session that can never complete, so the response reveals nothing about who is enrolled. Once the opt-in is on, every token request for the application needs a DPoP server nonce, browser sign-ins and refreshes included. Device protocol 8 carries it; the supported floor stays at 7. See In-app sign-in.

An id_token_hint is honoured only for the client it was issued to

/authorize accepted any hint this issuer had signed, so an ID token minted for one application could drive a silent (prompt=none) sign-in at another for the same person. The hint's aud (and azp, where it carries several audiences) must now name the requesting client. A hint that does not is ignored rather than refused: prompt=none answers login_required, and the response never reveals that the token was mis-audienced.

Console accounts: one sign-in method per account

A console account signs in with a password or with sign-in providers (SenseCrypt, GitHub, Google or Microsoft), never with both. An account created through a provider has no password, and Forgot password? sends it no email. The Forgot Password dialog shows the same sentence for every address and every kind of account. A provider cannot sign in to a password account, even when it reports the same email address. An account that had both before this release is now a password account. See Console sign-in methods.

3.2.0 — 2 October 2026

A tenant can run its own Authenticator app

A tenant whose users should install an app under the customer's own name, store listing and developer accounts can now register that app on the console's Mobile App page (GET and PATCH /v1/admin/mobile-app, capabilities mobile_app:read and mobile_app:update). The registration carries the app's name and custom URL scheme, its Android package name, signing-certificate fingerprints and required Play Integrity verdict, its iOS team and bundle IDs and App Attest environment, the store URLs, a sealed Google service-account key, and the app-link host. Attestation, push, the app-link documents, sign-in links, passkey origins, sign-in pages and end-user messages read it. App attestation becomes a per-tenant policy with off, audit and enforce modes; the deployment setting is now the default a tenant on the stock app inherits, and there is still no per-application opt-out. Push for such a tenant goes through its own Firebase project and is skipped, never misrouted, when no key is uploaded. The app-link documents at /.well-known/assetlinks.json and /.well-known/apple-app-site-association are served per host. The Android fingerprints become the tenant's accepted WebAuthn android:apk-key-hash origins. Sign-in links are minted on the app-link host, and a custom app's unset host defaults to the tenant's own host, never the shared stock host. The sign-in pages and end-user emails name the tenant's app and carry the application's branding. Switching app source requires confirmation, takes effect immediately, and activates attestation in audit. A tenant that stays on the SenseCrypt Authenticator is unchanged. See Ship your own Authenticator app, Running as the tenant's app and the error codes.

Face PKI: face-locked post-quantum keys, and ceremonies over CIBA

A tenant can now be created with face PKI (face_pki_enabled plus an ML-KEM and an ML-DSA parameter set, fixed at creation like the slug and the security profile). Every user of such a tenant has post-quantum keys in two families, ML-KEM and ML-DSA, one keypair of each per application and context, derived on the phone from their face token and their live face, so only their face unlocks them. An application registers a delivery endpoint (face_pki_delivery_endpoint, which needs a CIBA delivery mode) and asks for a key operation with a ceremony — a CIBA request carrying face_pki_kind (public_keys, sign or decapsulate), face_pki_context_b64, and for the last two a face_pki_payload_hash_b64 or face_pki_kem_ciphertext_b64; a binding_message and a client_notification_token are required on every ceremony. The user approves on the phone with a face scan; the phone delivers the signed result straight to the endpoint, and the ID token carries a face_pki receipt naming that delivery. The token endpoint and the push error payload return a ceremony's own failures by name — delivery_failed, key_unavailable, invalid_request and stale_token — instead of access_denied. The Management API gains the face_pki.* rule codes. The mobile SDK exposes PkiCeremonyFlow, and the signed device protocol moves to version 7. See Face PKI over CIBA, the Face-PKI ceremony flow, and the error codes.

Rotate a client's signing keys and certificates without breaking sign-in

Every key and certificate an application registers now has a lifetime, and six operations per kind under /v1/admin/oidc-apps/{client_id} — keys, trust-anchors, subject-dns and certificates — list, add, rotate, retire, reactivate and revoke it. A rotation (POST .../keys/rotate) makes the new key current at once and keeps the previous one verifying for a 24-hour grace window — or ends it immediately — across client assertions, JAR request objects and signed CIBA requests alike. A retired key can be reactivated for 30 days; a revoked key never verifies or registers again, not even if the client republishes it at its jwks_uri, and stays revocable after the application moves to one; the last current key cannot be removed. Every action is audited.

The same lifetime applies to a tls_client_auth application's trust anchors and allowed subject DN, and to a self_signed_tls_client_auth application's certificate: certificates issued by the old CA, or carrying the old subject, keep authenticating for the same 24-hour window. The whole-set PATCH of a JWK Set is unchanged — it still replaces instantly — except that it now refuses a revoked key.

For keys published at a jwks_uri, the issuer's cache drops from 1 hour to 5 minutes, and Refresh keys now makes every server re-read the URL immediately, so a key you remove stops working at once rather than up to an hour later.

In a FAPI 2.0 tenant an M2M application can authenticate without a secret — with private_key_jwt, or with its client certificate over mutual TLS, CA-issued (tls_client_auth) or self-signed (self_signed_tls_client_auth). Its Management API tokens are then DPoP-bound or certificate-bound, so they work only with a proof from that key or over mTLS with that certificate, and its credentials rotate through the same six operations. Secret-based M2M applications are unaffected. See Rotating client credentials and Machine-to-machine.

A tenant's RS256 key is published as soon as an application registers for it

Supersedes the lazy minting described under 3.0.0 below. A downstream broker — Entra External ID, Stytch, most of the developer-CIAM set — fetches jwks_uri while an operator is still configuring the connection. Because the RS256 key was minted on first use, the JWK Set held only a P-256 key at that moment, and the first RS256 ID token arrived carrying a kid the broker had never cached. Registering an application with id_token_signed_response_alg: RS256 now publishes the key immediately, and existing tenants that already had such an application are backfilled at startup. A tenant with no RS256 application is still not given an RS256 key.

Admin registration no longer accepts a password — breaking for direct API callers

POST /v1/admin/auth/register creates the unverified owner record with no credential and emails the verification link; the owner chooses their password when they redeem it, so POST /v1/admin/auth/verify-email now takes the token and the password and sets-and-verifies in one single-use claim. Opening the link no longer verifies anything on its own, so a clicked-but-abandoned link cannot strand an account verified without a password.

A request that still sends password to register is now refused with 422 and the field_moved rule code naming /v1/admin/auth/verify-email, where it used to be accepted and ignored. The console ships this change in the same release; a signup page loaded before it shipped gets the 422 until reloaded. This endpoint is a console surface and is not part of the published API reference.

3.1.0 — 28 September 2026

Every ID token carries a per-token jti

Every id_token now carries a jti claim — a per-token identifier, unique across every emission path: the authorization-code and refresh grants, CIBA and CIBA-push, and the implicit and hybrid front channel. OIDC does not require the claim, but a relying party that maps an ID token onto its own token identity needs one; Stytch's Trusted Auth Tokens path keys token_id on it. jti is a reserved claim — an attribute or claim-map entry of that name can neither forge nor override it — and it is advertised in claims_supported alongside sid. See Tokens and sessions.

3.0.0 — 22 September 2026

Implicit and hybrid sign-ins carry sid in the first id_token

An application registered for a non-code response type and a back-channel logout URL received an id_token without the sid claim on the first sign-in of a browser session; only a later silent re-authorization carried it. The claim is now there from the first sign-in, so the Logout Token your endpoint receives when that session ends names a session you were told about. Applications on the code flow were never affected, and sid is still omitted for an application that registers no back-channel logout URL. A sign-in refused at its last step — the user lost access to the application, or a requested scope was unbound or a requested API audience detached while the sign-in was in progress — now reaches your redirect URI as a standard error response (error=access_denied or invalid_scope, with your state) rather than a bare error response shown in the browser tab that your application never learned about. See Tokens and sessions and Relying party.

Application registration rules answer 422 with a field and a code — breaking for direct API callers

Every field-level refusal on the Management API now uses one envelope: 422 with details[], each entry naming the offending field and a stable rule code (fapi.requires_pkce, fapi_ciba.requires_signing_keys, jwks.private_material, immutable_field, extra_forbidden, …) — documented under Management API validation errors. On POST and PATCH /v1/admin/oidc-apps this replaces 24 refusals that were 400 bad_request with a prose message (the FAPI 2.0/CIBA registration rules, the mutual TLS and JWK Set checks, the CIBA delivery-mode rules, the refresh-token rotation rules, and the redirect-URI rules among them); the same rule now returns the same code on create and on update. Two further changes apply to every management body: an unknown field is refused (422, extra_forbidden) where it used to be silently ignored, and a field that is fixed at creation (client_type, token_endpoint_auth_method, billing_model, a tenant's slug or fapi_profile) is refused as immutable_field instead of being dropped. An integrator that branched on 400 as "business rule, show the user" versus 422 as "malformed payload" must read details[].code for both; successful responses are unchanged.

Tenant security profile set at creation; browser-session lifetime moves to the Issuer endpoint

A tenant's security profile (fapi_profile: Standard or FAPI 2.0/CIBA) is chosen when the tenant is created and cannot be changed afterwards; discovery is derived from it and every application in a FAPI tenant is held to it. The OP browser-session lifetime (op_session_ttl_seconds) is set on PATCH /v1/admin/issuer (capability issuer:update), alongside a read view of what discovery advertises and why. PATCH /v1/admin/tenants/{id} is rename-only and refuses both fields with the 422 validation envelope — immutable_field for fapi_profile, field_moved for op_session_ttl_seconds (see Management API validation errors). Neither field existed on the previous release, so no caller was setting them; the console uses the new surfaces. See Managing tenants.

Hybrid response types (per-application, off by default)

An application can now be registered for the OIDC hybrid response types — code id_token, code token and code id_token token — alongside code and the implicit types. The code flow remains the default and the recommended flow; a request for a type the application is not registered for is refused with unsupported_response_type, and an application on the FAPI 2.0 profile stays code-only. A hybrid response returns the authorization code and the front-channel material together in the URL fragment (or as a form_post); the code is still redeemed at the token endpoint with PKCE, a nonce is required for every hybrid type except code token, and the front-channel id_token binds the code with c_hash (and the access token with at_hash when one is returned). Redeeming a code twice revokes every token issued off it — the front-channel access token included — and signing the user out of your application, or suspending, deleting or renaming the user, ends the front-channel access tokens issued to it. Discovery now lists all six response types. See OIDC & OAuth 2.0, the validation recipe for the c_hash check, and the error codes.

Implicit response types and form_post (per-application, off by default)

An application can now be registered for the OIDC implicit response types — id_token and id_token token — alongside (or instead of) code, and any response can be delivered with response_mode=form_post. The code flow remains the default and the recommended flow; a request for a type the application is not registered for is refused with unsupported_response_type. Implicit responses ride the URL fragment (never the query), require a nonce, issue no refresh token, and bind the access token to the id_token with at_hash — see OIDC & OAuth 2.0, the validation recipe for the nonce / at_hash checks, and the error codes. Discovery now lists response_modes_supported.

CIBA request validation and callback contract — breaking

Two CIBA request rules changed incompatibly, and the callback behaviour is now documented as a contract.

  • binding_message is capped at 140 displayable characters (was 512) and refused with a new error code. The message is counted after Unicode NFC normalisation and must be plain text — control characters, line breaks and bidirectional formatting controls are refused; letters in any script, emoji and their joiner sequences are accepted (unassigned code points, such as a newer emoji, are no longer refused). A message that breaks the rule now returns invalid_binding_message (CIBA Core §13) instead of invalid_request. Clients that sent longer messages, or matched on invalid_request for this case, must adjust. See CIBA and the error codes.
  • client_notification_token is validated. It is required for ping and push delivery and, whenever supplied, must be a bearer token as RFC 6750 defines one (letters, digits, -._~+/, optional trailing =; no spaces or line breaks) of at most 1024 characters; anything else is invalid_request. Previously any non-empty value was accepted.
  • A signed authentication request (request JWT) is single-use. Its jti is remembered until the object expires; presenting the same object twice is invalid_request.
  • Callback contract for ping/push clients (now documented): a 3xx or 4xx from your notification endpoint is final — never retried, a redirect never followed — and for a push client it loses the approved authorization; 5xx and transport failures are retried up to 5 times in total. A refused request is notified to ping and push clients alike; an expired request is notified to push clients only (a ping client learns of the expiry from its own poll, which answers expired_token). An approval that lands in the last seconds of the request window is still delivered.
  • Push delivery is unavailable to FAPI-profile clients; a FAPI-profile issuer advertises backchannel_token_delivery_modes_supported: ["poll", "ping"].

FAPI 2.0 Security Profile and Message Signing

An application can be registered on the FAPI 2.0/CIBA security profile, and a tenant can be created so that every application is held to it. What the profile brings:

  • Sender-constrained tokens. DPoP is available to every client — send a proof, get back token_type: "DPoP" and an access token carrying cnf.jkt. Where discovery publishes mtls_endpoint_aliases, RFC 8705 mutual TLS is available too: the tls_client_auth and self_signed_tls_client_auth client-authentication methods and certificate-bound tokens (cnf["x5t#S256"]). Where the document does not publish the aliases, those methods and certificate binding are not available on that deployment. Introspection reports cnf and token_type.
  • Signed requests and responses. A signed request object (JAR) is accepted at the PAR endpoint only — /authorize answers request_not_supported for a request parameter and request_uri_not_supported for a non-PAR request_uri, and discovery says request_parameter_supported: false. JARM signs the authorization response itself: response_mode jwt, query.jwt, fragment.jwt and form_post.jwt return a single response JWT, errors included.
  • Profile rules at runtime. A FAPI application must push its authorization request, is code-only, authenticates with an asymmetric method, demonstrates its sender constraint on every token request, and gets authorization codes capped at 60 seconds.
  • A new protected resource. /v1/idp/oidc/resource is the issuer's own resource server: present an access token (Bearer or DPoP) and it answers the token's client_id, sub, scope, aud, exp, iat and token_type.
  • The client_credentials grant for a registered application, opt-in per application (allow_client_credentials, off by default). Not a breaking change: no registered OIDC application could mint through that grant before this release. The contract — what the flag gates, what the token carries, and what it is refused for — is on OIDC & OAuth 2.0 → Grant types.

New registration rules are refused as 422 entries with their own codes (fapi.*, fapi_ciba.*, jar.*, jwks.*, mtls.*). See OIDC & OAuth 2.0 and the error codes.

FAPI-CIBA: signed backchannel authentication requests

A CIBA client can send its authorization parameters as a signed request JWT at /bc-authorize, and a FAPI-profile client (or one registered with require_signed_request_object) must — a plain parameter request from such a client is invalid_request. The object's iss, aud, exp, iat, nbf and jti are all required, the jti is single-use, and discovery advertises the accepted algorithms as backchannel_authentication_request_signing_alg_values_supported. See CIBA.

Back-channel logout, with per-application initiators

An application that registers a backchannel_logout_uri now chooses which session endings send it a logout token (backchannel_logout_initiators). The vocabulary is six values. rp-logout is always on — registering the URI is that opt-in. The other five each default off, so no application is told about an ending it never asked about: session-revoked (a suspend, a SCIM active:false, or another user signing in on the same browser), access-revoked (the user lost access to this application through an authorization change — removed from a group that granted it, console or SCIM; the group deleted; or the group detached from the application), account-deleted, email-identifier-changed and session-expired. A list without rp-logout, or with an unknown member, is refused 422 (backchannel_logout_initiators.requires_rp_logout / .invalid). The authorization-change ending is the one whose behaviour is also new: losing an application through a group change already cut that application's refresh-token families, and now tells the application too — one logout token per live browser session of the user's that had signed in to it — while a user left in no app-attached group has their OP browser session ended as well, with every opted-in application in its sign-in list told. Device keys and passkeys are untouched either way; see Groups & access. Every logout token names the one session that ended with its sid — the OP never sends a sub-only token — and is typed logout+jwt and signed with the application's registered id_token algorithm. Discovery advertises backchannel_logout_supported and backchannel_logout_session_supported. The receiving-side checks are on the Relying party page.

OP browser session, prompt, max_age, RP-initiated logout, RS256 ID tokens and the claims parameter

  • An OP browser session (the sc_op_session cookie) is established by the face ceremony. While it is live a second application on the same tenant can be authorized without another ceremony. Its lifetime is absolute from the last ceremony — 8 hours by default, per-tenant on PATCH /v1/admin/issuer, and 0 turns browser sessions off entirely.
  • prompt and max_age now mean something. prompt=none is answered silently from that session or refused with login_required; login, consent and select_account force a fresh ceremony. max_age (and an application's default_max_age) is evaluated at /authorize, never at the token exchange. display and ui_locales are accepted and ignored.
  • RP-Initiated Logout ends the browser session and revokes the user's refresh-token families for every application that session signed in to. With a verified id_token_hint you are redirected to a registered post_logout_redirect_uri; without one the user confirms and lands on SenseCrypt's own signed-out page. GET and POST are both accepted.
  • ID tokens can be RS256 per application (id_token_signed_response_alg, ES256 by default) — a second per-tenant RSA key is created the first time one is needed, rotation covers both algorithms, and discovery advertises ["ES256", "RS256"]. Access tokens stay ES256. Anywhere the docs used to say "the id_token is an ES256 JWT", read "the algorithm your application registered".
  • The OIDC Core §5.5 claims request parameter is honoured at /authorize and PAR (claims_parameter_supported: true); it reshapes a release within the scopes your application may request and never widens one. The per-application id_token_includes_scope_claims switch (default on) decides whether scope-released profile claims ride in the ID token or only at UserInfo.
  • Protocol shape. /authorize accepts GET and POST; duplicate query parameters are refused; UserInfo speaks the RFC 6750 challenge on GET and POST (see the error codes); redeeming an authorization code a second time revokes every token that code's first use issued.

An id_token_hint must be an ID token — security

Every JWT a tenant signs verifies against the same JWKS, so signature and issuer alone used to let an access token or a logout token stand in for an ID token wherever a hint is accepted. A hint is now verified as an ID token on both halves of the JWS: the JOSE typ must be JWT (so logout+jwt, an at+jwt and a missing typ are all refused) and the payload must carry neither token_use nor events. A client that passed anything but a real ID token gets the 400 refusal page at /logout, invalid_request at /authorize and unknown_user_id at /bc-authorize; a genuine ID token, expired included, is unaffected. events is now a reserved claim name. A new attribute or claim-map output by that name is refused with a 422; an attribute that predates the gate is silently dropped — from every ID token, every UserInfo response and every SAML assertion — with no error raised, so a relying party that consumed the claim simply stops receiving it. Rename the attribute. See Customize claims and scopes.

Earlier releases

Passkeys and live-person sign-in (FIDO2/WebAuthn)

SenseCrypt sign-in now supports real FIDO2/WebAuthn passkeys (ES256). On the passkey method, a passkey proves the device and a live-face check proves the person. Three sign-in methods are available, all invisible to your OIDC/SAML integration:

  • Simple QR — scan an on-screen QR, then a face scan in the SenseCrypt Authenticator app, matched on the device.
  • Passkeys — a FIDO2/WebAuthn passkey through a roaming-authenticator app. This is the phishing-resistant path: WebAuthn binds each assertion to its origin, so a credential proven to one site can't be replayed against a look-alike.
  • Simple Webcam (enterprise only) — a webcam face scan on a trusted customer network, with no phone involved. Available to enterprise customers; contact sales@seventhsense.ai.

SenseCrypt still never stores your face image or a biometric template, only a sealed, unlinkable, non-reversible token derived from your face that even we cannot reverse, and which contains no PII. See How SenseCrypt works and the Passkey entry in the glossary.

Initial documentation release

The first public release of the SenseCrypt documentation, covering:

On this page

How we versionUnreleased3.7.0 — 29 October 2026Mobile SDK: a lost completion answer is re-sent once; iOS streams deliver the current state onceA suspended user who tries to add a phone is told soOne device per user; device protocol 12Signing out with an ID token from another session asks firstIn-app sign-in says when the phone's clock is wrongAdministrators can suspend and reinstate a user3.6.0 — 22 October 2026A new account joins a phone only with proof it runs on that phone; device protocol 11Administrators can revoke one of a user's devicesChoose how users are alerted about a backchannel requestConsole sign-in and forgot-password take the same time for every addressDelegated administrators adding a roleA CIBA token hint is honoured only for the application it was issued toThe Administrators page says why a request was refused3.5.1 — 16 October 2026The verify-email page says why a link was refused3.5.0 — 15 October 2026Mobile SDK: every shipped flow resumes after a lost answerAttestation under enforce now applies to enrolment; device protocol 10 is a forced update, breaking for Mobile SDK appsThe SenseCrypt eID documentation moved to /eid-docsKMS signing keys: rotation overlaps are billedDelegated administrators and shared rolesPKCE error names the setting3.4.0 — 8 October 2026Mobile SDK: in-app sign-in, face PKI during sign-in, and session refreshAccept DPoP-bound CIBA tokens on a FAPI 2.0/CIBA applicationFace PKI: tenant-scoped keys for allowed applications3.3.0 — 6 October 2026In-app sign-in for a tenant's own mobile appAn id_token_hint is honoured only for the client it was issued toConsole accounts: one sign-in method per account3.2.0 — 2 October 2026A tenant can run its own Authenticator appFace PKI: face-locked post-quantum keys, and ceremonies over CIBARotate a client's signing keys and certificates without breaking sign-inA tenant's RS256 key is published as soon as an application registers for itAdmin registration no longer accepts a password — breaking for direct API callers3.1.0 — 28 September 2026Every ID token carries a per-token jti3.0.0 — 22 September 2026Implicit and hybrid sign-ins carry sid in the first id_tokenApplication registration rules answer 422 with a field and a code — breaking for direct API callersTenant security profile set at creation; browser-session lifetime moves to the Issuer endpointHybrid response types (per-application, off by default)Implicit response types and form_post (per-application, off by default)CIBA request validation and callback contract — breakingFAPI 2.0 Security Profile and Message SigningFAPI-CIBA: signed backchannel authentication requestsBack-channel logout, with per-application initiatorsOP browser session, prompt, max_age, RP-initiated logout, RS256 ID tokens and the claims parameterAn id_token_hint must be an ID token — securityEarlier releasesPasskeys and live-person sign-in (FIDO2/WebAuthn)Initial documentation release