Launch on 01/01/2027Licenses available from 01/01/2027
PreviewThis page documents the release that launches on 01/01/2027. Packages, images and sign-up open then, and details may change before it.
Docs / Guides / Troubleshooting

Troubleshooting

Ready
TL;DR

Most failures are one of a handful of things. Work from the symptom you can see, not the stack trace. Three checks catch the majority; below that, the common symptoms with their real cause and fix. Every server error also carries a typed reason and a trace-id — those two turn a guess into a lookup.

Who this is for: whoever has something not working — an integration that will not connect, a phone that will not enroll, a challenge that never arrives. Organized by what you observe. For the full catalog of typed failure reasons, see Errors.

Start here — three checks#

In that order. They separate “the deployment is unwell” from “this one call is wrong” in under a minute.

What happens when something breaksNine failures, and what each one means for the three parties involved.WHAT HAPPENEDWHAT THE SERVER DOESWHAT THE USER SEESWHAT THE OPERATOR DOESDatabase unreachablerefuses everythingan error, and can retryrestore the databaseServer instance lostthe rest carry onnothing, behind a balancerreplace itCertificate expiredthat caller is refusedtheir integration failsrotate — minutesLicense expiredwarns for 30 days, then refuses callsnothing, then errorspull and apply a new oneClocks disagreenarrow tolerance, then refusean approval that will not takefix time syncPhone offlinethe request waitsopens the app to find itnothingPhone lostnothing, until recoverycannot authenticaterecovery revokes itRoot key compromisedkeeps runningnothing, until re-keyfull re-keyEvery row refuses rather than degradesThere is no failure in this table where the system continues while producing a weaker guarantee than itclaims. A component that cannot do its work stops doing it, which is the only behaviour anauthentication decision can safely have.
Eight failure scenarios: what the server does, what the user sees, and what the operator should look at.
1 · Is it up?

ydfi admin health — database pool, certificate validity, background reapers, in one line.

2 · Is it reachable, with mTLS intact?

A handshake failure is almost never a code bug. See the first symptom below.

3 · What did the server actually say?

Read ErrorDetail.reason — never the message text — and keep the trace_id.

ⓘ The two values worth capturing every time

The typed reason tells you which of the documented failures this is; the trace-id lets your operator find the exact call server-side. Together they replace guesswork. Both are on every error — see Errors.

Nothing connects at all#

It worked locally, then broke behind a load balancer or cloud front-end
transport: authentication handshake failed

What is happening

This is the single most common production failure, and it is not a bug. Ydentifi terminates mutual TLS at the server itself. Anything that terminates TLS in front of it — an application load balancer, Cloud Run, App Gateway, Front Door, a Cloudflare proxy or tunnel, Vercel, Netlify — strips the client certificate. The server then sees a connection with no client identity and fails closed. It does not silently downgrade: you get a failed handshake, not a weaker session.

Fix

Put the deployment behind an L4 / TCP-passthrough load balancer, or none at all — serving directly on :443 is the simplest correct setup. If you need a load balancer, it must pass TCP through untouched. See Deploy for the compatibility matrix.

The client certificate is rejected
MTLS_CERT_NOT_PINNED · MTLS_CERT_EXPIRED

What is happening

Either the deployment does not recognize your certificate, or it has passed its expiry. Relying-party certificates are long-lived by design (two years by default, up to five), so expiry usually means nobody was watching the date.

Fix

For an unrecognised certificate, have the operator re-provision it. For expiry, rotate: RotateCertificate issues a new one and keeps the old accepted for a grace window so you can hot-reload without dropping connections. Poll GetCertificateExpiry and warn yourself well ahead — there is deliberately no automatic renewal.

The phone cannot reach the deployment#

A phone reaches the local stack across your local network. Three things must hold: the phone and the laptop can see each other, the stack’s ports are published beyond loopback, and the server certificate carries the address the phone dials. Each entry below is one of them.

Same Wi-Fi, correct laptop IP, and it still refuses
connection refused · no route to host

What is happening

The local stack publishes its ports on loopback only — 127.0.0.1:50057, :50058. Nothing outside the machine can reach them, whatever IP the phone dials. Even after you change that, the server certificate only carries 127.0.0.1 by default, so a phone dialling your LAN address gets a name-mismatch and refuses the handshake. Two separate blockers, and fixing one still leaves the other.

Fix

Do both: publish the ports beyond loopback, and re-issue the certificates with your LAN address as a SAN via ydfi deploy local bootstrap --ip <your-LAN-IP>.

Both devices are on the same Wi-Fi but cannot see each other

What is happening

Many networks deliberately stop this. Client isolation (also called AP isolation) is standard on guest, hotel, café and a lot of corporate Wi-Fi — every device can reach the internet but not its neighbours. A VPN on either device does the same thing by routing traffic off the local segment, and the macOS firewall will silently drop inbound connections to Docker unless you approved the prompt.

Fix

Use a network that lets devices see each other. To diagnose it: drop the VPN on both devices, check System Settings → Network → Firewall, and try a plain ping from the phone — if that fails, the network is isolating clients and no amount of configuration on your side will change it.

It worked this morning and stopped after lunch

What is happening

Your laptop’s LAN address is handed out by DHCP and changes — after a sleep, a network switch, or a lease expiry. The certificate SAN and the enrollment QR both still point at the old address, so previously-working enrollments and any fresh scan now fail.

Fix

The certificate and the setup codes must name the new address: bootstrap again with it, then enrol again. Reserving the laptop’s address in your router (a DHCP reservation) stops it moving.

The phone shows Wi-Fi but nothing resolves

What is happening

A captive portal — hotel, airport, conference, guest network. The phone is associated to the access point but every request is being intercepted until someone accepts terms in a browser. The app has no way to show you that page, so it just fails.

Fix

Open any web page on the phone first and complete the portal. This one masquerades as a Ydentifi problem more often than it should.

The iOS Simulator never connects

What is happening

The app and the SDK connect only from a real device. Every connection checks the server against the pin in the setup code, and the simulator’s sandbox refuses the raw sockets the SDK’s transport uses.

Fix

Expected. Test on a real phone, which you need anyway: attestation and the depth-camera factors cannot run in a simulator.

Enrollment does not complete#

Scanning the QR does nothing, or reports the invite is no longer valid
ENROLLMENT_INVITE_EXPIRED

What is happening

Enrollment invites are single-use and short-lived — the window is 60 to 600 seconds and defaults to five minutes. A QR left on screen while someone finds their phone will lapse.

Fix

Mint a fresh invite with IssueEnrollmentInvite and raise ttl_seconds if your flow needs longer. Do not reuse a consumed one.

Enrollment is refused on a particular handset
DEVICE_COMPROMISED · attestation failure

What is happening

Every public action refuses to run on a device showing signs of system modification, and the server independently validates the platform attestation against Apple or Google. A jailbroken or rooted device, or an emulator pretending to be hardware, will not pass.

Fix

Expected behavior, not a defect — use an unmodified device.

The hardware is blocklisted
DEVICE_BANNED

What is happening

Someone banned this device’s hardware identifier. A ban targets the hardware, not one enrollment: it deactivates the active enrollment on that hardware, if there is one (at most one per relying party). The phone’s calls and open streams are refused with DEVICE_BANNED. Enrolling the same hardware again is refused with PERMISSION_DENIED “verification_failed” — the same answer a failed attestation gets, so the refusal does not reveal the ban. Your logs tell the two apart: the enrollment is logged as complete_enrollment_blocked_by_ban.

Fix

If the ban was wrong, lift it with UnbanDevice — but note that does not restore the old enrollments. The user re-enrolls from a fresh invite.

Challenges never arrive, or expire first#

The challenge expires before the user can respond
AUTH_CHALLENGE_STATUS_EXPIRED

What is happening

Challenge lifetime is bounded to 30–300 seconds and defaults to 60. Sixty seconds is tight if the user has to find their phone, unlock it, and read a prompt. Expiry is a status, not an error: AwaitChallenge ends with a final frame whose status is AUTH_CHALLENGE_STATUS_EXPIRED, and a phone that answers afterwards is refused with CHALLENGE_ALREADY_TERMINAL.

Fix

Raise ttl_seconds when you issue — 120 is a comfortable default for a human-in-the-loop action. The expiry is enforced server-side, so the value you send is the value that binds.

The user has no device to receive it
NO_ACTIVE_DEVICE_FOR_USER

What is happening

The identity exists but has no active enrolled device — either it was never enrolled, or every device was revoked, banned, or cascaded away by a recovery invite.

Fix

Check with ListDevicesForUser (it returns inactive rows too, so you can see what happened), then onboard a device. Remember that issuing a recovery invite revokes every active device by design.

The device refuses because it cannot meet the required tier
DEVICE_INCAPABLE (at issuance) · AUTH_STRENGTH_INSUFFICIENT (at submit)

What is happening

DEVICE_INCAPABLE means the server refused before prompting: the device physically cannot do what you asked — for example a signed biometric on a handset with no strong biometric enrolled. AUTH_STRENGTH_INSUFFICIENT means it could have at issuance but did not at signing time: Face ID switched off, a wet sensor, a failed biometric.

Fix

For the first, lower min_strength for that action or have the user enroll a capable device. For the second, re-challenge — it is usually transient. Never treat either as a reason to accept a weaker result than you asked for.

You issued a challenge twice and the second call failed
CHALLENGE_ALREADY_TERMINAL · CHALLENGE_ALREADY_EXISTS

What is happening

ALREADY_TERMINAL means it was already approved, denied, or dismissed — terminal states cannot be re-driven. ALREADY_EXISTS is the idempotency signal for a retry after a network loss, not a fault.

Fix

Read the terminal outcome rather than re-issuing. For ALREADY_EXISTS, treat it as success and de-duplicate on your side.

A user is locked out#

One user suddenly cannot do something; everyone else is fine
USER_LOCKED · RATE_LIMIT_EXCEEDED

What is happening

Two different things, told apart by the reason. USER_LOCKED: the relying party locked this user with UpdateUserStatus, and every call for them is refused until it sets them ACTIVE again. RATE_LIMIT_EXCEEDED: the user emptied a per-user token bucket, and that one action is refused for at least 30 minutes; their other actions still work. The error’s auto_expires_at says when it is admitted again.

Fix

For USER_LOCKED, set the user ACTIVE if the lock is no longer needed. For RATE_LIMIT_EXCEEDED, wait, or clear it deliberately with UnlockUserRequestLimits once you understand why it tripped. If it keeps happening, the relying party’s rate tier may be wrong for your usage — three tiers are selectable per relying party. See Limits & quotas.

Codes, clocks and licenses#

TOTP codes fail intermittently for one user
valid = false, with a correct-looking code

What is happening

Usually the code was already used. Each code is accepted once: a double submit, a retry after a timeout, or two sign-ins within one 30 s step all send a used code. After a newer code is accepted, an older one is refused too. Otherwise, clock drift: TOTP is time-derived, so a device minutes out of step generates codes for the wrong window.

Fix

Never resend a code; ask the user for the next one. For drift, widen the tolerance with the skew parameter on VerifyUserTotp — the default is ±1 step (30 s), and the most is ±2. Fix the underlying clock where you can; widening skew trades a little security for tolerance.

Every TOTP verification starts failing for everyone
TOTP_SECRET_KEY_UNAVAILABLE

What is happening

The secrets are encrypted with a key the server no longer holds — an operator removed it from the key configuration while secrets still used it.

Fix

If the operator still has the removed key, add its pair back to the configuration next to the pairs set now — keep the newer ones — and restart: every secret is readable again. Otherwise re-mint each affected secret with IssueUserTotpEnrollment(reuse_existing=false). This is a configuration incident, not a code path you can retry around.

Calls start being refused after a period of normal operation
LICENSE_EXPIRED · LICENSE_USER_CAP_REACHED

What is happening

Either the license expired more than 30 days ago, or you reached the seat count (a free license stops at 50 users). Seat refusals are fail-closed, not throttles.

Fix

Check standing with ydfi license status. The deployment never renews over the network: to renew, run ydfi pull, then ydfi license apply. Requests keep working for 30 days after expiry, then are refused until a new license is installed; see Get a license.

Getting diagnostics#

Four places to look, in increasing depth:

# 1. is the deployment healthy right now?
$ ydfi admin health

# 2. license seats, usage and expiry
$ ydfi license status

# 3. metrics for your own monitoring (admin listener)
$ curl -s https://your-deployment/metrics

Fourth: the trace-id on any error. It is the one string that lets whoever runs the deployment find the exact call in their tracing — no personal data changes hands, and it turns a support conversation into a lookup. See Operate & maintain for what to monitor day to day.

⚠ Do not go around the API

When something looks wrong, the temptation is to open the database and inspect or correct rows directly. Don’t. Every mutation has safeguards, audit-chain entries, and license accounting attached to it; a direct write bypasses all three and leaves the deployment in a state nothing else expects. Use the CLI and the API — see Operate & maintain.

Opens on 01/01/2027

Available at launch

Until then, you can try the whole ceremony on your own phone.