---
title: "Create Session"
method: POST
path: "/v1/auth/session"
tags: ["auth"]
---

# Create Session

`POST /v1/auth/session`

Verify a sign-in credential and set the HttpOnly session cookie.

The session is bound to the identity that authenticated, so every request it
later authenticates resolves a user and that user's active organization
rather than only "a credential was presented once". The response names both,
so a client knows who it is signed in as without a second call.

The rate-limit check deliberately runs only after a failed verification,
not before it: a pre-verification gate can't know whether *this* attempt
would have succeeded, so once an IP has used up its failure quota it
would end up blocking that IP's legitimate owner too, not just further
attackers. Running after verification also means the throttle bounds how
many verdicts an IP gets, not how much work it can cause: a password attempt
pays for a bcrypt verification (cost 12, on the order of 200ms of CPU, and
one is burned against a stand-in hash even for an address nobody holds)
before the limit is consulted, so a 429 costs the same as a 401. A gateway
exposed to the internet should rate-limit this path at the proxy as well.

The maintenance-mode check runs before either credential is verified, and
refuses both. Before, because a frozen deployment should not spend a bcrypt
verification per attempt and the refusal is not about the credential
anyway; both, because the way back out is the master key against
``PATCH /v1/settings/maintenance-mode`` through the header, which never
passes through this door. That is what keeps the way back out off the frozen
path, and it is why no identity needs an exemption here; an operator who no
longer holds the master key recovers by setting ``OTARI_MASTER_KEY`` and
restarting, which is a restart rather than a click. It leaks nothing
either: ``GET /v1/bootstrap`` already publishes the same flag
unauthenticated, so the sign-in screen can render the right page.

## Request body

- CreateSessionRequest — Sign in to the dashboard with exactly one credential. A flat body with an optional field per credential, rather than a tagged union: it is one extra key on the wire, it generates a client type a hand-written form can fill in, and the validator below makes the two forms exclusive anyway. The example carries one credential, because a generated example is a body somebody will post: the schema alone would produce every field at once, which is the one shape the validator below refuses.
  - `email` string, nullable — The identity's sign-in address.
  - `master_key` string, nullable — The gateway master key; verified once and never stored by the browser. Accepted only while the operator identity has no password, which is to say while nobody has claimed this deployment (see GET /v1/bootstrap).
  - `password` string, nullable — The identity's password.

## Response `200`

Successful Response

- SessionResponse — A freshly minted dashboard session (the token travels only in the cookie).
  - `active_organization_id` string, uuid, required — The organization that identity is acting in, which scopes every tenancy surface.
  - `expires_at` string, date-time, required — When the session cookie stops being accepted.
  - `user_id` string, uuid, required — The identity this session speaks for.

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/mozilla-ai/apis/otari.md) · [All operations](https://skmtc.dev/mozilla-ai/apis/otari/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/mozilla-ai/otari/revisions/7f178e92b56c/schema)
