Email codes
POST /v1/email-codes and POST /v1/email-codes/verify: request a 6 digit code by email and sign in with it.
The fallback and recovery path when a face does not match. The user asks for a code, FaceCard emails it, and the user sends it back to sign in. The idea and the rules are explained in Email code sign in.
Credential: publishable key, from an allowed Origin, for both routes. Neither route needs a challenge.
Request a code
POST /v1/email-codes
{ "email": "ada@example.com" }| Field | Type | Rules |
|---|---|---|
email | string | A valid email address. Matched against your app's users without regard to case. |
curl -X POST https://api.facecard.dev/v1/email-codes \
-H "X-FaceCard-Key: fc_pk_live_xxx" \
-H "Origin: https://app.example.com" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com"}'Response
202 Accepted
{ "expires_at": "2026-10-14T12:10:00.000Z" }The answer is always 202, whether or not a user with that email exists in your app. This stops the route from being used to find out who is enrolled. If a user exists, FaceCard sends a 6 digit code to the address and the email subject names your app. If no user exists, nothing is sent and nothing is stored. Do not word your screen so that it reveals which case happened.
The code is valid for 10 minutes and works once. Asking for a new code retires the older pending code for that user.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body is malformed or email is missing or not an address. |
| 401 | unauthorized | Missing or invalid key. |
| 403 | origin_not_allowed | The Origin is not on the allow list. |
| 429 | rate_limited | Over a limit (see below). A Retry-After header gives the seconds to wait. |
| 503 | email_unavailable or email_send_failed | A user exists and the email could not be sent. Try again later. |
Verify a code
POST /v1/email-codes/verify
{ "email": "ada@example.com", "code": "123456" }| Field | Type | Rules |
|---|---|---|
email | string | The address the code was sent to. |
code | string | The 6 digit code from the email. |
curl -X POST https://api.facecard.dev/v1/email-codes/verify \
-H "X-FaceCard-Key: fc_pk_live_xxx" \
-H "Origin: https://app.example.com" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","code":"123456"}'Response
200 OK
{
"user": {
"id": "usr_...",
"email": "ada@example.com",
"name": "Ada",
"email_verified": true,
"created_at": "2026-10-14T12:00:00.000Z"
},
"token": "<jwt>",
"expires_at": "2026-10-14T13:00:00.000Z",
"method": "email_code"
}- The token is a normal user token with
amr: ["otp"]andemail_verified: true. - If the user's email was not verified before, this call verifies it.
- A successful verify counts as one authentication toward your plan.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body is malformed. |
| 400 | invalid_code | The code is wrong, expired, already used, or there is no code for that email. One generic message for all of these, so the response never says which. |
| 401 | unauthorized | Missing or invalid key. |
| 403 | origin_not_allowed | The Origin is not on the allow list. |
| 429 | rate_limited | Over a limit, or the user is locked out after too many failures. |
| 429 | plan_limit_reached | The team used all its auths for the month. |
Five wrong attempts kill the code, and the user must request a new one. Wrong codes also count toward the per user lockout (5 failures in 15 minutes, the same as failed face matches). While locked out, verify answers 429 rate_limited even for a correct code.
Limits
All answer 429 rate_limited with Retry-After.
| Route | Limit |
|---|---|
| Request a code | 10 per 15 minutes per IP, and 5 per hour per email address per app (counted even when no user exists) |
| Verify a code | 20 per 15 minutes per IP |
| Request a code, per app | Shares the per minute request budget with match, plus a daily cap of 5% of the plan's monthly auths (at least 50) |
See Plans and limits for the rest.
In the SDK
SDK 4.2.0 or later: requestEmailCode(email) and signInWithEmailCode(email, code). See Browser client.