Auth API
buildingSign somebody in with a code or a password, read the session back, and end it.
Sign in
Two ways in, and every account has the first one.
A code always works. It is how accounts are created, so every user has it without setting anything up. A password works if they have set one, which is optional — see sign up.
If you want the flow people expect from Google and everybody else, ask for the address first, then ask for whatever that account can answer. Your app knows which by trying a password sign-in and reading the error, or simply by offering both.
The code path, in two calls — the same pair that creates an account. Ask for a
code with send-verification-otp, then:
1curl -X POST "$BASE/v1/auth/sign-in/email-otp" \2 -H "authorization: Bearer $PK" \3 -H "content-type: application/json" \4 -d '{"email":"ada@example.com","otp":"481920"}'Same response shape as below: a session cookie and a token. Full flow in
sign up, because for a new address it is the sign-up.
Takes a publishable key.
1curl -X POST "$BASE/v1/auth/sign-in/email" \2 -H "authorization: Bearer $PK" \3 -H "content-type: application/json" \4 -d '{"email":"ada@example.com","password":"a-long-password"}'1{2 "redirect": false,3 "token": "onSnRF8wZMuCs03Li7lcKJT2tLTvHjQO",4 "user": { "id": "Nrzmwy…", "email": "ada@example.com", "emailVerified": true }5}The session arrives as a Set-Cookie. The token in the body is the same
session for clients that cannot hold cookies — a native app, say.
If the account has two-factor enabled, no session comes back. You get this instead, and send them to your second-factor screen:
{ "twoFactorRedirect": true, "twoFactorMethods": ["totp"] }See two-factor for the call that finishes it.
Who the caller is, or null. This is the call your app makes on load.
1curl "$BASE/v1/auth/get-session" \2 -H "authorization: Bearer $PK" \3 -b "session cookie"1{2 "session": {3 "id": "h0bsPL9wBPaRfvslAcU2q0w8VPeF4wJu",4 "expiresAt": "2026-08-26T14:22:39.655Z",5 "activeOrganizationId": null6 },7 "user": { "id": "Nrzmwy…", "email": "ada@example.com", "emailVerified": true }8}A missing or expired session answers 200 with null rather than 401. Not
being signed in is an ordinary state for this call, not a failure.
1curl -X POST "$BASE/v1/auth/sign-out" \2 -H "authorization: Bearer $PK" \3 -b "session cookie"Ends the session it was called with and clears the cookie. Other sessions that person has — another browser, their phone — are left alone.
Errors
| Status | Code | Means |
|---|---|---|
401 | INVALID_EMAIL_OR_PASSWORD | Wrong password, no password set, or no such account |
400 | INVALID_OTP | Wrong or expired code |
403 | EMAIL_NOT_VERIFIED | The address is unconfirmed — always required, see settings |
403 | — | Publishable key used from an unregistered origin |
429 | — | 3 attempts per 10 seconds per IP; see limits |
503 | — | The project's auth database is still being created |
One message covers a wrong password, an account with no password, and an unknown address, on purpose. Anything more specific turns this form into a way to test who has an account with you — and "that account has no password" would say a great deal about somebody who never chose to tell you.
EMAIL_NOT_VERIFIED re-sends the confirmation link as it refuses, so the way out
is already in their inbox by the time they read the error. New accounts never see
it — they were created by proving the address — so it only shows up for accounts
that predate that change.