Skip to content
Hardcount
Run it locally
Pre-1.0 · one maintainer · Stripe and cards only · no SDKs (client libraries) yet · no production users yet. Docs describe v0.9.1 (29 Oct 2026) · What’s new →

Sign in with email and password; mint an httpOnly session cookie

POST
/v1/auth/login
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.

Media typeapplication/json
object
email
required
string format: email
password
required
string

Example generated

{
"email": "hello@example.com",
"password": "example"
}

Session minted; cookie set on response

Media typeapplication/json
object
user_id
required
string
tenant_id
required
string
email
required
string
livemode
required
boolean
expires_at
required
string format: date-time

Example generated

{
"user_id": "example",
"tenant_id": "example",
"email": "example",
"livemode": true,
"expires_at": "2026-04-15T12:00:00Z"
}

Invalid email or password

Media typeapplication/json
object
error
object
type

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.

string
code
string
message
string
request_id
string
param

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.

string

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 and Retry-After gives 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.
Media typeapplication/json
object
error
object
type

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.

string
code
string
message
string
request_id
string
param

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.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}
Retry-After
integer

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.

Media typeapplication/json
object
error
object
type

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.

string
code
string
message
string
request_id
string
param

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.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}