FaceCard Docs
Concepts

Email code sign in

The fallback and recovery path: a 6 digit code sent to the user's email, how it works, its limits and what it proves.

Face sign in can fail for ordinary reasons. The person has no camera, the light is poor, their face changed, or they are on a new device. A browser also cannot prove that a live person is in front of the camera (see Liveness is advisory). So every app needs a second way back in.

FaceCard's second way is email code sign in. The user asks for a 6 digit code, FaceCard emails it to the address they enrolled with, and the user types it in. A correct code signs them in and returns the same kind of user token as a face sign in.

This page explains the idea. The two routes are documented in API reference: email codes. The browser SDK methods (SDK 4.2.0 or later) are in Browser client.

The flow

  1. The user cannot sign in with their face and enters their email.
  2. Your page calls POST /v1/email-codes with that email (the SDK method is requestEmailCode(email)).
  3. FaceCard answers 202 with an expiry time. If a user with that email exists in your app, FaceCard sends them a 6 digit code. The subject of the email names your app.
  4. The user reads the code and enters it.
  5. Your page calls POST /v1/email-codes/verify with the email and the code (the SDK method is signInWithEmailCode(email, code)).
  6. On success you get { user, token, expires_at, method: "email_code" }. You send the token to your server and verify it like any other, as in Quickstart: server.

An email code sign in does not need a challenge. Both routes use your publishable key and need the request Origin to be on your app's allow list, like the other browser routes.

Rules of a code

RuleValue
Length6 digits
Lifetime10 minutes
UseSingle use. A used code never works again.
Wrong attempts5, then the code is dead
Newer codeAsking again retires the older pending code for that user

FaceCard stores only an HMAC of the code, never the code itself.

It always answers 202

POST /v1/email-codes returns 202 whether or not that email has a FaceCard in your app. An attacker cannot use the route to find out who is enrolled. When no user exists, FaceCard sends nothing and stores nothing.

Your screen should say the same thing either way, for example "If that email has an account, we sent a code." Do not tell the person that no account was found.

If the email service fails while a user does exist, the route returns 503 email_unavailable or email_send_failed and the message says to try again later. That can reveal that the address has an account during an outage. It is a deliberate trade, and it only happens when sending fails.

Limits

All limits answer 429 rate_limited with a Retry-After header.

ScopeLimit
Code requests per IP address10 per 15 minutes
Code requests per email address per app5 per hour (counted even when no user exists, so the limit itself reveals nothing; no address is stored for this, only a keyed hash)
Verify requests per IP address20 per 15 minutes
Failed attempts against one user5 failures in 15 minutes lock that user out of verify for the rest of the window. Wrong codes count, the same as failed face matches.
Code requests per appShare the app's per minute request budget with match, plus a daily cap of 5% of the plan's monthly auths (at least 50)

A successful code sign in counts as one authentication toward your plan, the same as an enroll or a match. Requesting a code does not count. See Plans and limits.

How it verifies the email

A face sign in proves that the person looked like an enrolled face. It says nothing about the email, because anyone can enroll with any address. Reading a code sent to that address is the proof.

When a code is accepted and the user's email was not yet verified, FaceCard marks it verified. From then on, every token issued for that user carries email_verified: true. An email change that is confirmed with its code also verifies the new address.

So email_verified tells you whether the address has ever been proven by receiving a code. It does not mean the person used a code on this particular sign in. For that, read amr.

amr: how the user signed in

Every token has an amr claim (authentication methods references, RFC 8176 values):

Sign inamr
Enroll, match, and adding templates["face"]
Email code sign in["otp"]

Use amr when an action needs to know which method was used. For example, you may allow a lower privilege session after an email code sign in, or ask for a face check before a sensitive action. tokens.verify, verifyTokenOffline and POST /v1/tokens/verify all return it. See User tokens and JWKS.

What it means for your accounts

  • Key your own accounts on sub together with app_id. Never key them on email.
  • Treat email as a contact address. Trust it only when email_verified is true. Before that, anyone could have enrolled with it.
  • Whoever can read the mailbox can sign in as that user with a code. An email code is exactly as strong as the user's email account, so treat an ["otp"] session accordingly.

Recovery

If a user's face no longer matches, or they are on a new device, they can still sign in with an email code. Your app gets a valid token for the same user, so their profile and your own data for that user stay reachable. There is nothing extra to print or store: the user's email is the recovery path.

An email code sign in does not enroll a new face or add a face template. Adding a template (POST /v1/me/templates) still needs a fresh face match, as described in /v1/me routes.

If a user has lost access to their email too, FaceCard cannot recover the account. Your app may have its own support process for that case.

On this page