# Authentication — Finradar API > Version: 3.61.0 | Generated: 2026-06-20 | Content Hash: 584b4c09 > Fetch this file at: https://uat.finradarapi.com/llms/authentication.txt ## Authentication All endpoints require an API key. Pass it via query parameter `?apiKey=YOUR_KEY` or header `X-API-Key: YOUR_KEY`. WebSocket endpoints accept the key in the `token` auth payload or query parameter. --- ## Authentication Manage user sessions and credentials. ### POST /api/v1/auth/register Register a new user account. EXEMPT (`cost: 0`). INVITE-ONLY during the soft-trial — body must include a valid `invite_token` from POST /api/v1/auth/redeem-invite. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `message` (string): Top-level result message. `Register user successful` on success (200). On error: `Email already exists` (400), `Invalid or expired invite token` (400), `Invite code is used and cannot be used` (410), or the underlying exception message (400). UI should toast on success and gate-form on error. - `linkMsg` (string): Email-flow status message. Always `Please verify the email, I have send verification link.` on a clean 200. If verification email send failed (SMTP error), the endpoint returns 500 instead with a different message and the user is created but cannot log in until they trigger /auth/resend-verification. **Since:** v1.0.0 **Utility:** Create a new user account during the invite-only soft-trial. EXEMPT (`cost: 0`) — auth endpoints never debit tokens. Phase 64 enforces invite-token gating: the body MUST include a short-lived `invite_token` JWT issued by `/api/v1/auth/redeem-invite`; the server re-validates the token against the `invite_codes` table on every register call (never trusts the JWT alone). On success the user is created with the FREE tier (2,000 tokens/month), an API key is generated, a verification email is sent, AND a welcome email with the API key + `curl` example is sent (separate try/except so SMTP failure does NOT 500 the response). The user CANNOT log in until they click the verification link in the email — `/auth/login` returns 400 if `verify_email=false`. **Use case:** Frontend signup form: user enters email + password + redeemed invite code → POST here → backend creates the account, sends verification email, returns success message. User clicks verification link → email is verified → user can log in. Server-side onboarding scripts can also call this with a programmatically-redeemed invite token (see /auth/redeem-invite). **Parameters:** - `email` (body, required): User email address. Lowercased server-side. Must be unique across the `users` table — re-registration with an existing email returns 400 with `Email already exists`. Used as the canonical login identifier and the destination for the verification email. - `password` (body, required): User password — minimum 8 characters (validated by `@validate_register`). Hashed via bcrypt server-side; never stored in plaintext, never returned in any response. Use a strong password manager-generated value in production; the example shown above is a redaction placeholder. - `invite_token` (body, required): Short-lived JWT from POST /api/v1/auth/redeem-invite. Encodes the underlying `invite_codes.code` value; the server re-validates against the `invite_codes` table to confirm the code is still REDEEMABLE (not used, not expired, not revoked) before creating the account. Required during the Phase 64 invite-only soft-trial. **Sample response:** ```json { "message": "Register user successful", "linkMsg": "Please verify the email, I have send verification link." } ``` ### POST /api/v1/auth/login 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). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `message` (string): 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): 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): 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): API key for server-to-server calls (32-char concatenation of two UUIDs with hyphens removed). Pass as `X-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): 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): 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 (nullable)): 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): JWT access token issued by `flask_jwt_extended.create_access_token`. Pass as `Authorization: Bearer ` 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). **Since:** v1.0.0 **Utility:** 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 ` 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. **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. **Parameters:** - `email` (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). - `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. **Sample response:** ```json { "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": { "id": 42, "userId": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "total_limit_api": 100, "reach_limit_api": 12, "plan": "free", "status": "active", "current_period_end": null, "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z" } }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwZjE0..." } ``` ### POST /api/v1/auth/forgetpassword Initiate the password-reset flow. Sends a one-time reset link to the registered email. EXEMPT (`cost: 0`). Token expires after 24 hours. Always returns 200 with a generic message regardless of whether the email is registered (anti-enumeration). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `message` (string): Generic status message. Always identical across the registered / unregistered / already-sent / SMTP-failed cases to prevent enumeration. UI should show a 'check your email' toast on every 200. **Since:** v1.0.0 **Utility:** Send a password-reset email containing a one-time JWT token. EXEMPT (`cost: 0`). Tokens expire after 24 hours; if a previous unexpired token already exists for the user, the existing token is re-used (no duplicate email). The link in the email points at GET /api/v1/auth/verifyPasswordLink which 302-redirects to the FinRadar frontend's `/reset-password?token=…` page; the frontend then PUTs `{token, newpassword}` to /api/v1/auth/newpassword to finalize. The redirect target is server-controlled — there is no caller-supplied redirect-URL parameter (the previous `weblink` field was removed in the 2026-05-18 security hardening because it enabled an open-redirect that could exfiltrate the user UUID). **Use case:** Frontend 'Forgot password?' form: user enters email → POST here → generic 'check your email' toast (always 200, never 400) → user checks email → clicks link → 302 to frontend `/reset-password?token=…` page → user enters new password → frontend PUTs to /api/v1/auth/newpassword to finalize. **Parameters:** - `email` (body, required): Email address. Lowercased server-side. The endpoint ALWAYS returns 200 with a generic message regardless of whether the email is registered — this is intentional and prevents account enumeration. Only a registered user actually receives an email. **Sample response:** ```json { "message": "If that email is registered, a password-reset link has been sent." } ```