Product
API reference
The JSON routes behind the dashboards, documented from the route handlers themselves. Every route enforces its own session, role and plan gate; the table says which.
For contractors: API access is designed for agency and carrier books and is available on Core and larger plans. Your checklist and uploads stay in the Flora dashboard.
Authentication
Every request is authenticated by the same session cookie your browser gets when you sign in (flora_session, HttpOnly). A script that signs in with POST /api/auth/login and keeps the cookie can call every route below that its role allows.
State-changing requests (POST) must also carry an Origin or Referer header naming this deployment, and form posts must carry the csrf field rendered on the page. JSON callers get the same answers with an Accept: application/json header.
API keys are not issued yet. Access is by session cookie only; when a key mechanism ships, this page will describe it.
Conventions
- Money is an exact decimal: { "cents": "<digits>", "currency": "USD" }. Never a floating-point number.
- Errors are { "detail": "<message>" } with the status in the table. 401 no session · 403 wrong role · 402 the plan does not include the feature (the body carries "upgrade": "/billing/pricing") · 404 not in your view · 422 invalid input · 429 throttled.
- Every response is Cache-Control: no-store. Dates are ISO 8601 strings.
Example
# sign in (keeps the cookie in jar.txt); the csrf value comes from the login page
curl -c jar.txt -b jar.txt -H 'Accept: application/json' \
-H 'Origin: https://<this deployment>' \
-d 'email=you@example.com' -d 'password=…' -d 'csrf=<token from /login>' \
https://<this deployment>/api/auth/login
# then read your book
curl -b jar.txt -H 'Accept: application/json' https://<this deployment>/api/bookCompany file
| Method | Path | Who | Plan gate | Parameters | Returns |
|---|---|---|---|---|---|
GET | /api/book | any signed-in | alerts_digest for the alert column and book_filters for the query filters (agent, carrier, ops); without them alert is null / filters are ignored and alerts_gated / filters_gated say so | query: class, state, min_score, max_score (optional filters) | 200 { role, scope_note, disclosure, alerts_gated, filters_gated, rows: [{ app, signals, missing, alert, score, verification }] } scoped to the caller. 422 invalid filter. |
GET | /api/policies | any signed-in | json_api (Core and up; ops always); 402 with upgrade: /billing/pricing otherwise | query: class, state, min_score, max_score (optional filters) | 200 [{ policy_id, tenant_id, business_name, class_code, state, ..., score }] scoped to the caller. 422 invalid query. |
GET | /api/policies/{policyId} | any signed-in | — | path: policyId | 200 { app, checklist, documents, score, verification, ... } for one policy. 404 when the policy is outside the caller’s view. |
GET | /api/policies/{policyId}/score | any signed-in | — | path: policyId | 200 { policy_id, score, reasons: [{ signal, impact, detail }], computed_at }; computes and stores a score when none exists. 404 outside the caller’s view. |
POST | /api/policies/{policyId}/documents | insured, agent, ops | — | multipart/form-data: file (required), item_key, period_start (YYYY-MM-DD), period_end, replaces_id | 201 { document, score } after the quality checks. 400 file missing or bad fields. 413 too large. 422 the document contradicts the file. |
GET | /api/policies/{policyId}/readiness-signals | ops | — | path: policyId | 200 the exact readiness snapshot and linked agent/carrier recipients available for review before sending. |
POST | /api/policies/{policyId}/readiness-signals | ops | — | JSON: { audiences: ["agent" | "carrier"], idempotencyKey } | 201 persisted and audited readiness snapshot; 200 duplicate replay. Email delivery is best-effort. |
POST | /api/policies/{policyId}/requests | agent, ops | — | JSON: { item_key (required), question } | 201 the recorded document request. 400 item_key missing or question not text. |
POST | /api/policies/{policyId}/verify | ops | — | JSON: { provider ("fake" default or "twilio"), answers, answer_call, reply_texts }; header Idempotency-Key (one value, ≤128 chars) | 200 { confirmed, attempts, discrepancies, scope_note, ... }. 400 malformed body. 409 same key, different payload. 422 ambiguous key. |
GET | /api/audit/{policyId} | carrier, ops | — | path: policyId | 200 the append-only audit rows for one policy, oldest first. 404 outside the caller’s view. |
GET | /api/readiness-signals | agent, carrier | — | none | 200 { signals } containing only snapshots addressed to the caller role on linked policies. |
Alerts and exports
| Method | Path | Who | Plan gate | Parameters | Returns |
|---|---|---|---|---|---|
GET | /api/alerts | agent, carrier, ops | alerts_digest | query: likelihood = low | medium | high (optional) | 200 [{ policy_id, likelihood, message, evidence, estimate: { value, basis, label }, kind, disclosure, created_at }]. 402 plan lacks the digest. 422 bad likelihood. |
GET | /api/export.csv | carrier, ops | export_csv | none | 200 text/csv of the caller’s book. 402 plan lacks the export. |
Billing
| Method | Path | Who | Plan gate | Parameters | Returns |
|---|---|---|---|---|---|
GET | /api/billing/status | any signed-in | — | query: tenant_id (ops only; ignored for everyone else) | 200 { tenant_id, entitlement: { status, plan, trial_ends_at, active_until, features, ... } | null }. |
GET | /api/billing/ops | ops | — | query: tenant_id (optional; absent = every tenant) | 200 one tenant’s entitlement with recent webhook deliveries, or the list of tenants. 400 unknown_tenant. |
POST | /api/billing/ops | ops | — | JSON: { tenant_id, status, plan, active_until, trial_ends_at, reason, reference?, confirm } | 200 { ok, step: "preview" } with nothing written until confirm is true, then { ok, step: "applied" } (audited). 400 unknown_tenant. |
POST | /api/billing/webhook | Polsia | HMAC in X-Flora-Signature (POLSIA_BILLING_SECRET); not a user route | JSON event from the checkout partner (docs/billing-polsia-bridge.md) | 200 { ok, duplicate | stale }. 400 bad signature, schema or unknown_tenant. 503 secret not configured. |
POST | /api/billing/stripe/webhook | Stripe | HMAC in Stripe-Signature (STRIPE_WEBHOOK_SECRET); backup payment path, not a user route | Stripe event JSON (docs/billing-fallback.md) | 200 { ok, applied | duplicate | ignored | refused }. 400 bad signature or not an event. 503 secret not configured. |
Sign-in and account
| Method | Path | Who | Plan gate | Parameters | Returns |
|---|---|---|---|---|---|
POST | /api/auth/signupform post · csrf | public | — | form: company, name, email, password, csrf | 303 to /login (or 200 { ok, next } for JSON). The answer is identical for a new and an existing address. 400 weak password or bad input. 503 unavailable. |
POST | /api/auth/loginform post · csrf | public | — | form: email, password, csrf, next (same-site path) | 303 to the dashboard with the flora_session cookie (200 { user, next } for JSON). 401 invalid. 429 locked after repeated failures. |
POST | /api/auth/logoutform post · csrf | any signed-in | — | form: csrf, all=1 (every session) | 303 to /login with the cookie cleared (200 { ok } for JSON). |
GET | /api/auth/session | public | — | none | 200 { user: { id, tenantId, role, email, name } | null }. |
POST | /api/auth/sessions/revokeform post · csrf | any signed-in | — | form: id (one of the caller’s own sessions), csrf | 303 to /account (200 { ok, current } for JSON); audited as session_revoked. 403 not the caller’s session. |
POST | /api/auth/passwordform post · csrf | any signed-in | — | form: current, password, csrf | 303 to /account (200 { ok } for JSON); other sessions are signed out. 401 wrong current password. 400 weak password. |
POST | /api/auth/forgotform post · csrf | public | — | form: email, csrf | 303 to /forgot?notice=sent (200 { ok, next } for JSON), identically for a registered, unknown, disabled or throttled address. A single-use one-hour link is minted and handed to the email provider; with none configured it waits at /ops/email. |
POST | /api/auth/reset/issueform post · csrf | ops | — | form: email, csrf | 303 to /reset/issue with the one-hour link (201 { token, path, expiresAt } for JSON). 404 no_user. |
POST | /api/auth/resetform post · csrf | public | — | form: token, password, csrf | 303 to the user’s dashboard with a fresh flora_session cookie (200 { ok, next } for JSON); every session that existed before is signed out and every other open reset is retired. 403 reset_invalid (unknown or expired) / reset_used. 400 weak_password, with the link still usable. |
GET | /api/auth/verify | public | — | query: token (from the confirmation email) | 303 to /login?notice=verified; the single-use token is the authorisation, so no session or csrf field is needed. 303 /login?error=verify_invalid for an unknown, used or expired token. |
POST | /api/auth/verify/resendform post · csrf | any signed-in | — | form: csrf | 303 to /account?notice=verify_sent (200 { ok, next } for JSON), the same answer whether a link went out, the address was already confirmed, or the throttle swallowed it. |
POST | /api/auth/inviteform post · csrf | insured, ops | — | form: email, role, tenant_id, agent_id, policy_id, csrf (an insured may only invite agent/carrier into its own tenant) | 303 to /invite with the single-use link, which is also emailed to the invitee (201 { token, path, expiresAt } for JSON). 400 bad input. 403 unverified when the inviter has not confirmed their own address. |
POST | /api/auth/invite/acceptform post · csrf | public | — | form: token, name, password, csrf | 303 to the new user’s dashboard with the session cookie (201 { user, next } for JSON). 401 bad or used token. |
POST | /api/themeform post · csrf | public | — | form: theme = clear | deep, back (same-site path), csrf | 303 back with the flora_theme cookie set. |
POST | /api/auth/mfa/verifyform post · csrf | public | the signed flora_mfa challenge cookie the team login sets after a correct password | form: code (six digits, or a recovery code), csrf | 303 to /ops with the session cookie set and the challenge cleared (200 { user, next } for JSON). 401 mfa_invalid back to /team/login/verify on a wrong code; mfa_expired back to /team/login without a live challenge; locked after repeated failures. |
POST | /api/auth/mfa/enrollform post · csrf | ops | — | form: csrf | 303 to /account/mfa, which now shows the QR and manual key for a pending secret (200 { ok, uri, manual_key } for JSON). 409 mfa_state when a factor is already on. 503 mfa_unconfigured without FLORA_MFA_KEY. |
POST | /api/auth/mfa/confirmform post · csrf | ops | — | form: code, csrf | 200 the eight recovery codes rendered once on this response (200 { ok, recovery_codes } for JSON); from here the team login requires a code. 401 mfa_invalid on a wrong first code. 409 mfa_state with nothing pending. |
POST | /api/auth/mfa/disableform post · csrf | ops | — | form: password, code (current or recovery), csrf | 303 to /account/mfa?notice=mfa_disabled (200 { ok } for JSON); the factor and every recovery code are removed and audited. 401 mfa_invalid when either proof fails. |
Contact
| Method | Path | Who | Plan gate | Parameters | Returns |
|---|---|---|---|---|---|
POST | /api/contactform post · csrf | public | rate limit: 5 messages per address per hour | form: name, email, topic, message, company, role, plan, tenant_id, csrf (topic account_deletion needs a session) | 303 to /contact?sent=1 (201 { ok, id } for JSON); stored and audited. 422 invalid or boundary. 429 limited. |
POST | /api/contact/handledform post · csrf | ops | — | form: id, csrf | 303 to /ops/inbox (200 { ok } for JSON); audited as contact_handled. 404 unknown id. |
AI
| Method | Path | Who | Plan gate | Parameters | Returns |
|---|---|---|---|---|---|
POST | /api/ai/suggestionform post · csrf | insured, agent, ops | — | form: id, policy_id, decision (accepted | dismissed), csrf | 303 back to the caller’s dashboard with ?ai=accepted|dismissed (200 { ok, decision } for JSON); audited as ai.suggestion_accepted / ai.suggestion_dismissed. 404 unknown id, a suggestion already decided, or a policy outside the caller’s scope. |
POST | /api/ai/settingsform post · csrf | any signed-in | — | form: tenant_id, enabled (true | false), back, csrf | 303 back with ?ai_saved=1 (200 { ok, enabled } for JSON); audited as ai.settings_changed. 403 for a tenant_id that is not the caller’s own, unless the caller is ops. |
POST | /api/ai/chatform post · csrf | any signed-in | — | form or JSON: question, thread_id (empty starts a new conversation), csrf | 303 back to /chat?thread=… (200 { ok, thread_id } for JSON). 400 error=too_short, 404 error=no_thread for a conversation that is not the caller’s, 429 error=quota when the plan’s allowance for that day is spent. The answer itself is written to the thread, never returned here. |
POST | /api/ai/chat/threadform post · csrf | any signed-in | — | form or JSON: id, action (rename | delete), title (rename only), csrf | 303 back to /chat with ?ok=renamed|deleted (200 { ok } for JSON). 404 for a conversation belonging to another company or another person, which is the same answer as one that never existed. |
Operations
| Method | Path | Who | Plan gate | Parameters | Returns |
|---|---|---|---|---|---|
POST | /api/auth/email/revealform post · csrf | ops | — | form: id (an outbound_emails row), csrf | 303 back to /ops/email carrying the one-time link once (200 { url } for JSON); always audited as email_reveal, naming the ops user, the recipient and that user’s tenant. url is null when the message holds no link. |
GET | /health | public | — | none | 200 { status: "healthy" }. The deploy pipeline’s healthcheck. |
GET | /api/flora-verify | operator | FLORA_VERIFY_KEY in ?key=; answers 404 without it | query: key | 200 the behavioural self-check report. 404 key absent or wrong. |
GET | /api/ai/health | ops | — | none | 200 { enabled, send_document_text, active_provider, providers[], usage } — which AI provider is answering and today’s tokens against the daily budget. 401 without a session, 403 for any role but ops. |
POST | /api/gtm/touchform post · csrf | ops | — | form: id, kind (call | email | note), csrf; optional status, status_notes, next_action (YYYY-MM-DD), note, back | 303 back to the go-to-market view (200 { ok, status } for JSON). Updates one internal prospect and appends one activity row. 403 for any role but ops, or for a malformed field. 404 unknown id. |
POST | /api/gtm/outreach/draftform post · csrf | ops | — | form: id, csrf | 303 back to the prospect page with ?outreach=drafted (200 { ok, draft } for JSON). Writes one outreach email draft for the prospect from its row and the approved-facts block, appends an activity note and an audit row. 403 for any role but ops, or when the row names an excluded party (nothing is written). |
POST | /api/gtm/outreach/draft-allform post · csrf | ops | — | form: csrf | 303 back to /ops/gtm with ?outreach=drafted_all&drafted=&skipped=&blocked= (200 { ok, drafted, skipped, blocked } for JSON). Writes a deterministic outreach draft for every prospect that does not have one yet; blocked rows are skipped. One audit row, no per-row activity. |
POST | /api/gtm/outreach/saveform post · csrf | ops | — | form: id, draft_id, subject, body, csrf | 303 back to the prospect page with ?outreach=saved, or ?outreach=claims when the edit contains a claim Flora cannot make (kept, but not sendable). 403 for any role but ops. |
POST | /api/gtm/outreach/sendform post · csrf | ops | — | form: id, draft_id, to, confirm (yes), csrf; optional test (1: to the support inbox only) | 303 back to the prospect page with ?outreach=sent (200 { ok, emailId, status, provider, to } for JSON). Queues one outbound email from support@florawc.com with the support inbox as reply-to and blind copy, logs it on the prospect, audits it. Refused with nothing queued (409 for JSON) when the postal address is not on file, the recipient is not an address, the box was not ticked, the daily cap is reached, the words fail the claims check, or the draft was already sent; 403 when the row or recipient names an excluded party. |
POST | /api/gtm/outreach/settingsform post · csrf | ops | — | form: id, postal_address, csrf | 303 back to the prospect page with ?outreach=address_saved. Stores the postal address every outreach email carries; sending is refused until it is set. 403 for any role but ops, or for an address with no number. |
POST | /api/ops/vaultform post · csrf | ops | — | form: name, value, csrf; optional notes | 303 back to /ops/vault (200 { ok, id, name } for JSON). Stores one Flora-owned secret, encrypted in Postgres, and audits the write. 403 for any role but ops. 503 vault_unconfigured when FLORA_VAULT_KEY is not set. |
POST | /api/ops/vault/revealform post · csrf | ops | — | form: id, csrf | 200 the plaintext rendered once on the POST response (200 { id, name, value } for JSON), always audited naming the ops user. 404 unknown id. 403 for any role but ops. 503 vault_unconfigured. |
POST | /api/ops/vault/deleteform post · csrf | ops | — | form: id, csrf | 303 back to /ops/vault (200 { ok } for JSON). Removes one secret and audits the deletion. 404 unknown id. 403 for any role but ops. |
AI on the API
The AI routes take a form post and render a page; there is no streaming endpoint and no chat state. Every AI field in a response carries the source it came from, and a response whose claims could not be traced is returned without them rather than with them unsourced.
Every element a model wrote carries an “AI draft” marker and, underneath it, the file and page it read and the words it relied on. If there is no marker, no model was involved. What Flora’s model does, and what it does not.
Nothing the API returns is a coverage decision. Scores, signals and verification results are observations for the carrier to weigh.
Flora is a data vendor, not an insurer, agent, or underwriter. Flora reports evidence-backed observations; the carrier makes every coverage decision. Flora does not price, issue, place, or end insurance coverage and does not recommend coverage decisions. Nothing on this site is an offer of insurance, an insurance application, or a coverage recommendation.
Which plan unlocks the gated routes: Plans. How sessions and tenancy are enforced: Security. Stuck: contact Flora.