<!-- section: Auth API · status: building · source: docs/auth-api/03-sign-in.md -->

> Sign 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](/docs/auth-api/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.

## POST /v1/auth/sign-in/email-otp

The code path, in two calls — the same pair that creates an account. Ask for a
code with `send-verification-otp`, then:

```bash Request
curl -X POST "$BASE/v1/auth/sign-in/email-otp" \
  -H "authorization: Bearer $PK" \
  -H "content-type: application/json" \
  -d '{"email":"ada@example.com","otp":"481920"}'
```

Same response shape as below: a session cookie and a `token`. Full flow in
[sign up](/docs/auth-api/sign-up), because for a new address it *is* the sign-up.

## POST /v1/auth/sign-in/email

Takes a **publishable** key.

```bash Request
curl -X POST "$BASE/v1/auth/sign-in/email" \
  -H "authorization: Bearer $PK" \
  -H "content-type: application/json" \
  -d '{"email":"ada@example.com","password":"a-long-password"}'
```

```json Response
{
  "redirect": false,
  "token": "onSnRF8wZMuCs03Li7lcKJT2tLTvHjQO",
  "user": { "id": "Nrzmwy…", "email": "ada@example.com", "emailVerified": true }
}
```

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:

```json
{ "twoFactorRedirect": true, "twoFactorMethods": ["totp"] }
```

See [two-factor](/docs/auth-api/two-factor) for the call that finishes it.

## GET /v1/auth/get-session

Who the caller is, or `null`. This is the call your app makes on load.

```bash Request
curl "$BASE/v1/auth/get-session" \
  -H "authorization: Bearer $PK" \
  -b "session cookie"
```

```json Response
{
  "session": {
    "id": "h0bsPL9wBPaRfvslAcU2q0w8VPeF4wJu",
    "expiresAt": "2026-08-26T14:22:39.655Z",
    "activeOrganizationId": null
  },
  "user": { "id": "Nrzmwy…", "email": "ada@example.com", "emailVerified": true }
}
```

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.

## POST /v1/auth/sign-out

```bash Request
curl -X POST "$BASE/v1/auth/sign-out" \
  -H "authorization: Bearer $PK" \
  -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](/docs/auth-api/settings) |
| `403` | — | Publishable key used from an unregistered origin |
| `429` | — | **3 attempts per 10 seconds per IP**; see [limits](/docs/auth-api/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.
