Skip to content
/api/v1/auth/login

Authenticate with email + password and receive a JWT access token.

Authenticate with email + password and receive a JWT access token. EXEMPT (`cost: 0`). Bad credentials return 401 with a single generic message (anti-enumeration). Per-account brute-force lockout: 5 / 10 / 20 consecutive failures trigger a 1-min / 5-min / 1-hour lockout respectively (HTTP 429 with `Retry-After` header).

Why use this

Exchange email + password for a JWT access token (`flask_jwt_extended`-issued). EXEMPT (`cost: 0`). The returned token is a Bearer JWT — pass as `Authorization: Bearer <token>` to protected dashboard endpoints (NOT to API-key endpoints; those use the `X-API-Key` header from the user's `api_key` field). Each successful login appends a session_id to the user's `user_login_device` array and is bounded by `device_limit` (default 2 — third login evicts the oldest session). Email must be verified (`verify_email=true`) before login succeeds; otherwise 400 with a re-send-verification message. The user object returned in the body includes the API key — surface this in the dashboard 'API Keys' page so the user can copy it for server-to-server calls. **Lockout (issue #272):** five consecutive wrong-password attempts triggers a 60s lockout; tenth a 5-min lockout; twentieth a 1-hour lockout. During lockout every login returns HTTP 429 with a `Retry-After` header — even calls supplying the correct password (intentional, so an attacker can't fish for whether they've guessed right). A successful login resets the counters.

Common use case

Frontend login form posts here → receives JWT + user object → stores JWT in cookie or localStorage → uses Bearer auth on protected endpoints (`/api/v1/account/*`, `/api/v1/user/*`, `/api/v1/billing/*`). Server-side automation should NOT call this; use the user's `api_key` directly instead (no expiry, no device limit, no brute-force lockout). On 429 the SPA should honour `Retry-After` and surface a 'Too many attempts, retry in N seconds' toast; do NOT auto-retry inside the lockout window.

Email + password authentication. EXEMPT (cost: 0). Returns a Bearer JWT for use against protected dashboard endpoints, plus the full user object (mirroring GET /api/v1/user/). Successful logins are bounded by the user's device_limit (default 2 concurrent sessions — a third login evicts the oldest). Email verification is REQUIRED — unverified users get 400 and a fresh verification email is sent server-side. Response includes the user's api_key for the 'API Keys' dashboard page; remember the JWT and the API key are two distinct auth mechanisms (JWT for browsing the dashboard, API key for billed server-to-server calls). For password reset use POST /api/v1/auth/forgetpassword; to register a new account use POST /api/v1/auth/register.

Parameters

NameInRequiredDefaultAllowedDescriptionExample
emailbodyrequiredRegistered email address. Lowercased server-side before lookup. Returns 401 `Invalid email or password` whether the email is unregistered or the password is wrong (anti-enumeration since the 2026-05-18 hardening pass).user@example.com
passwordbodyrequiredUser password (6+ chars). Compared against the bcrypt hash stored in `users.password`. Returns 401 `Invalid email or password` on mismatch (same generic message as unknown-email so an attacker cannot distinguish the two). Never logged, never echoed in responses.<redacted>

Response schema

FieldTypeNullableDescription
messagestringnoTop-level result. `User login successfully.` on 200. On error: `Invalid email or password` (401, identical for unknown email AND wrong password — anti-enumeration); `Please verify the email, I have send verification link.` (400, when email is unverified — server side-effect: re-issues a verification email if the previous link expired); `Too many failed login attempts. Try again later.` (429, after 5/10/20 consecutive wrong-password attempts — response also carries a `Retry-After` header in seconds).
userobjectnoFull user profile (same shape as GET /api/v1/user/ returns). Includes the user's `api_key` — surface in the dashboard 'API Keys' page so they can copy it for server-to-server calls. Includes embedded `Userplan` (subscription state) and `credit_balance` (Stripe-paid top-up balance, in USD).
user.uuidstringnoStable user identifier (UUIDv4 string). Used as the JWT `sub` claim and as the foreign-key target in every per-user table (`user_token_ledger.user_id`, `token_transactions.user_id`, etc.). Treat as the canonical user PK for client-side state.
user.api_keystringnoAPI key for server-to-server calls (32-char concatenation of two UUIDs with hyphens removed). Pass as `X-API-Key: <api_key>` to billed endpoints. Distinct from the JWT `token` field — the API key has NO expiry and NO device limit, but DOES debit tokens; the JWT expires (default 1h), is bound to a session_id, but does NOT debit tokens (session cookies are exempt).
user.verify_emailbooleannoTrue if the user has clicked the verification email link. Always true in a successful login response (login is gated by this flag — unverified users get a 400 instead of a 200). Use to dispatch onboarding-state UI client-side.
user.credit_balancenumbernoStripe-paid top-up balance in USD (NOT in cents — server returns `float(credit_balance)`). Used by [POST /api/v1/billing/quota](/docs/account/billing-and-subscription/post-billing-quota) for buying additional API quota at 100 requests / $1. Distinct from `current_balance` in [GET /api/v1/account/balance](/docs/account/token-pricing/get-account-balance) — that's the token ledger; this is the dollars wallet.
user.UserplanobjectyesEmbedded subscription state (relationship-included via `to_dict(include_relations=True)`). Null for accounts where the relationship row is missing (legacy edge case). Contains `total_limit_api`, `reach_limit_api`, `plan` (`free`/`weekly`/`monthly`/`pro`/`yearly`), `status`, `current_period_end`.
tokenstringnoJWT access token issued by `flask_jwt_extended.create_access_token`. Pass as `Authorization: Bearer <token>` on protected endpoints. Embeds the user's `uuid` as `sub` and a `session_id` claim used for device-limit eviction. Default expiry is the Flask app's `JWT_ACCESS_TOKEN_EXPIRES` (typically 1 hour). Re-issue by re-calling /auth/login (no separate refresh-token endpoint in v1).

Sample response

·
  • "message": "User login successfully."
  • "user":
    • "id": 42
    • "uuid": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2"
    • "email": "user@example.com"
    • "usertype": "user"
    • "api_key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
    • "verify_email": true
    • "is_online": true
    • "has_uat_access": false
    • "billing_admin": false
    • "credit_balance": 0
    • "notify_email": true
    • "notify_browser": true
    • "webhook_url": null
    • "created_at": "2026-04-15T10:00:00.000Z"
    • "updated_at": "2026-05-02T15:51:00.000Z"
    • "Userplan":
    }
  • "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwZjE0..."
}

Errors

StatusLabelDescription
200OKRequest succeeded.
400Bad RequestInvalid query, body, or path parameter.
401UnauthorizedMissing or invalid Authorization header / api_Token.
402Payment RequiredInsufficient token balance for this call. Top up
429Too Many RequestsRate limit exceeded for your tier (see /pricing for tier limits). Tier limits
500Server ErrorUnexpected server-side failure. Retry with backoff; report if persistent.

Code samples

curl -X POST "https://api.finradar.ai/api/v1/auth/login" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Generate an API key in /account/credentials to run live queries (literal YOUR_API_KEY placeholder shown until then).