/api/v1/auth/loginAuthenticate 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
Common use case
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
| Name | In | Required | Default | Allowed | Description | Example |
|---|---|---|---|---|---|---|
| body | required | — | — | Registered 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 | |
| password | body | required | — | — | User 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
| Field | Type | Nullable | Description |
|---|---|---|---|
| message | string | no | Top-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). |
| user | object | no | Full 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.uuid | string | no | Stable 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_key | string | no | API 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_email | boolean | no | True 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_balance | number | no | Stripe-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.Userplan | object | yes | Embedded 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`. |
| token | string | no | JWT 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
| Status | Label | Description |
|---|---|---|
| 200 | OK | Request succeeded. |
| 400 | Bad Request | Invalid query, body, or path parameter. |
| 401 | Unauthorized | Missing or invalid Authorization header / api_Token. |
| 402 | Payment Required | Insufficient token balance for this call. Top up |
| 429 | Too Many Requests | Rate limit exceeded for your tier (see /pricing for tier limits). Tier limits |
| 500 | Server Error | Unexpected 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).