Sign in with email and password; mint an httpOnly session cookie
const url = 'http://localhost:8080/v1/auth/login';const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"email":"hello@example.com","password":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:8080/v1/auth/login \ --header 'Content-Type: application/json' \ --data '{ "email": "hello@example.com", "password": "example" }'Validates email + password, mints a server-side session row, and
sets Set-Cookie: hardcount_session=...; HttpOnly; SameSite=Lax; Path=/.
Subsequent dashboard requests authenticate via the cookie; sessions
are bound to users.id and are independent of API key lifecycle.
See ADR-011.
A successful sign-in also sets hardcount_device (HttpOnly,
Path=/v1/auth, 180 days). It marks the browser as one that signed
in to this email, so the sign-in throttle counts its attempts on
their own. It never replaces the password.
Request Bodyrequired
Section titled “Request Bodyrequired”object
Example generated
{ "email": "hello@example.com", "password": "example"}Responses
Section titled “Responses”Session minted; cookie set on response
object
Example generated
{ "user_id": "example", "tenant_id": "example", "email": "example", "livemode": true, "expires_at": "2026-04-15T12:00:00Z"}Invalid email or password
object
object
One of invalid_request_error, authentication_error,
rate_limit_error or api_error. Every error the API
answers has this shape, including an unknown path (404,
code route_not_found) and a method the path does not take
(405, code method_not_allowed, with an Allow header).
The few exceptions are listed in docs/api-surface.md.
The request field the error is about. Sent only when the error
names one. A request body may hold only the fields its
operation lists, spelled and cased as listed: any other
field, or one the server sets (such as id), is refused
with 422 and param naming it. An operation with no request body lists no fields: send
no body, {} or null. The Stripe and Postmark webhooks and
the LiteLLM callback take their sender’s body as it comes.
Example
{ "error": { "type": "invalid_request_error" }}Too many attempts. Two limits answer 429, and neither locks the account:
- Code
login_throttled: the sign-in throttle. Repeated wrong passwords for one email, from one address or from many, start a wait of 1 to 15 minutes. The message names the wait in minutes andRetry-Aftergives it in seconds. The answer is the same for an email with no account. A browser that signed in to the email before is counted on its own. - Code
rate_limited: the shared per-address request limit on/v1/auth.
object
object
One of invalid_request_error, authentication_error,
rate_limit_error or api_error. Every error the API
answers has this shape, including an unknown path (404,
code route_not_found) and a method the path does not take
(405, code method_not_allowed, with an Allow header).
The few exceptions are listed in docs/api-surface.md.
The request field the error is about. Sent only when the error
names one. A request body may hold only the fields its
operation lists, spelled and cased as listed: any other
field, or one the server sets (such as id), is refused
with 422 and param naming it. An operation with no request body lists no fields: send
no body, {} or null. The Stripe and Postmark webhooks and
the LiteLLM callback take their sender’s body as it comes.
Example
{ "error": { "type": "invalid_request_error" }}Headers
Section titled “Headers”Seconds until an attempt is counted again.
Code authentication_unavailable: the sign-in throttle could not
read the database, so the attempt was refused before the password
check. Try again shortly.
object
object
One of invalid_request_error, authentication_error,
rate_limit_error or api_error. Every error the API
answers has this shape, including an unknown path (404,
code route_not_found) and a method the path does not take
(405, code method_not_allowed, with an Allow header).
The few exceptions are listed in docs/api-surface.md.
The request field the error is about. Sent only when the error
names one. A request body may hold only the fields its
operation lists, spelled and cased as listed: any other
field, or one the server sets (such as id), is refused
with 422 and param naming it. An operation with no request body lists no fields: send
no body, {} or null. The Stripe and Postmark webhooks and
the LiteLLM callback take their sender’s body as it comes.
Example
{ "error": { "type": "invalid_request_error" }}