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/book

Company file

MethodPathWhoPlan gateParametersReturns
GET
/api/bookany signed-inalerts_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 soquery: 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/policiesany signed-injson_api (Core and up; ops always); 402 with upgrade: /billing/pricing otherwisequery: 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-inpath: policyId200 { app, checklist, documents, score, verification, ... } for one policy. 404 when the policy is outside the caller’s view.
GET
/api/policies/{policyId}/scoreany signed-inpath: policyId200 { 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}/documentsinsured, agent, opsmultipart/form-data: file (required), item_key, period_start (YYYY-MM-DD), period_end, replaces_id201 { 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-signalsopspath: policyId200 the exact readiness snapshot and linked agent/carrier recipients available for review before sending.
POST
/api/policies/{policyId}/readiness-signalsopsJSON: { audiences: ["agent" | "carrier"], idempotencyKey }201 persisted and audited readiness snapshot; 200 duplicate replay. Email delivery is best-effort.
POST
/api/policies/{policyId}/requestsagent, opsJSON: { item_key (required), question }201 the recorded document request. 400 item_key missing or question not text.
POST
/api/policies/{policyId}/verifyopsJSON: { 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, opspath: policyId200 the append-only audit rows for one policy, oldest first. 404 outside the caller’s view.
GET
/api/readiness-signalsagent, carriernone200 { signals } containing only snapshots addressed to the caller role on linked policies.

Alerts and exports

MethodPathWhoPlan gateParametersReturns
GET
/api/alertsagent, carrier, opsalerts_digestquery: 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.csvcarrier, opsexport_csvnone200 text/csv of the caller’s book. 402 plan lacks the export.

Billing

MethodPathWhoPlan gateParametersReturns
GET
/api/billing/statusany signed-inquery: tenant_id (ops only; ignored for everyone else)200 { tenant_id, entitlement: { status, plan, trial_ends_at, active_until, features, ... } | null }.
GET
/api/billing/opsopsquery: 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/opsopsJSON: { 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/webhookPolsiaHMAC in X-Flora-Signature (POLSIA_BILLING_SECRET); not a user routeJSON 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/webhookStripeHMAC in Stripe-Signature (STRIPE_WEBHOOK_SECRET); backup payment path, not a user routeStripe 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

MethodPathWhoPlan gateParametersReturns
POST
/api/auth/signupform post · csrfpublicform: company, name, email, password, csrf303 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 · csrfpublicform: 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 · csrfany signed-inform: csrf, all=1 (every session)303 to /login with the cookie cleared (200 { ok } for JSON).
GET
/api/auth/sessionpublicnone200 { user: { id, tenantId, role, email, name } | null }.
POST
/api/auth/sessions/revokeform post · csrfany signed-inform: id (one of the caller’s own sessions), csrf303 to /account (200 { ok, current } for JSON); audited as session_revoked. 403 not the caller’s session.
POST
/api/auth/passwordform post · csrfany signed-inform: current, password, csrf303 to /account (200 { ok } for JSON); other sessions are signed out. 401 wrong current password. 400 weak password.
POST
/api/auth/forgotform post · csrfpublicform: email, csrf303 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 · csrfopsform: email, csrf303 to /reset/issue with the one-hour link (201 { token, path, expiresAt } for JSON). 404 no_user.
POST
/api/auth/resetform post · csrfpublicform: token, password, csrf303 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/verifypublicquery: 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 · csrfany signed-inform: csrf303 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 · csrfinsured, opsform: 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 · csrfpublicform: token, name, password, csrf303 to the new user’s dashboard with the session cookie (201 { user, next } for JSON). 401 bad or used token.
POST
/api/themeform post · csrfpublicform: theme = clear | deep, back (same-site path), csrf303 back with the flora_theme cookie set.

Contact

MethodPathWhoPlan gateParametersReturns
POST
/api/contactform post · csrfpublicrate limit: 5 messages per address per hourform: 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 · csrfopsform: id, csrf303 to /ops/inbox (200 { ok } for JSON); audited as contact_handled. 404 unknown id.

AI

MethodPathWhoPlan gateParametersReturns
POST
/api/ai/suggestionform post · csrfinsured, agent, opsform: id, policy_id, decision (accepted | dismissed), csrf303 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 · csrfany signed-inform: tenant_id, enabled (true | false), back, csrf303 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.

Operations

MethodPathWhoPlan gateParametersReturns
POST
/api/auth/email/revealform post · csrfopsform: id (an outbound_emails row), csrf303 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
/healthpublicnone200 { status: "healthy" }. The deploy pipeline’s healthcheck.
GET
/api/flora-verifyoperatorFLORA_VERIFY_KEY in ?key=; answers 404 without itquery: key200 the behavioural self-check report. 404 key absent or wrong.
GET
/api/ai/healthopsnone200 { 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.

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.