GET /api/discover
Filter locations by score thresholds across all metros.
Query parameters
| Name | Type | Required | Default | Description |
|---|
metro | string | | | Metro slug filter. |
compositeMin | int | | | Minimum composite score. |
businessMin | int | | | Minimum Business Vitality. |
safetyMin | int | | | Minimum Safety score. |
limit | int | | 50 | Max results. |
Example
curl "https://axiomlocus.io/api/discover?metro=sf&compositeMin=60&safetyMin=50"
Response
{ "total": 12, "results": [{ "h3_index": "...", "metro_slug": "sf", "location_name": "Mission District", "composite": 74 }] }
| Field | Type | Description |
|---|
total | number | Number of matching locations |
results[].composite | number | Composite score 0-100 |
results[].location_name | string | Neighborhood name |
GET /api/nearby
Everything we know near a location — permits, POIs, schools, clinical trials, FDA events, zoning.
Query parameters
| Name | Type | Required | Default | Description |
|---|
lat | float | ✓ | | Latitude. |
lng | float | ✓ | | Longitude. |
Example
curl "https://axiomlocus.io/api/nearby?lat=37.7749&lng=-122.4194"
Response
{ "summary": { "permits": 45, "pois": 120, "schools": 3, "clinical_trials": 8 }, "recent": { ... } }
| Field | Type | Description |
|---|
summary | object | Count of records per data type within ~1km |
recent | object | Recent records from each data type |
GET /api/metro-tier-distribution
Count of scored cells in each safety tier bucket for a given signal across a metro. Powers the tier-mix bar on the Explorer Intelligence Rail so you can see at a glance whether a metro is mostly safe or mostly elevated, without leaving the page.
Buckets follow the standard composite score breakpoints: Prime ≥80, Strong 60–79, Solid 40–59, Watch 20–39, Elevated <20. Cells with a null value for the requested signal are excluded.
Query parameters
| Name | Type | Required | Default | Description |
|---|
metro | string | ✓ | | Metro slug (e.g. sf, nyc, denver). |
signal | string | | safety_environment | Column to bucket on. One of composite, business_vitality, population_momentum, demographics, economic_strength, development_pipeline, accessibility, safety_environment, amenity_demand. |
Example
curl "https://axiomlocus.io/api/metro-tier-distribution?metro=sf&signal=safety_environment"
Response
{
"metro": "sf",
"signal": "safety_environment",
"total": 1284,
"buckets": {
"prime": 412,
"strong": 537,
"solid": 246,
"watch": 71,
"elevated": 18
}
}
| Field | Type | Description |
|---|
metro | string | Metro slug echoed from the request. |
signal | string | Signal column the distribution is computed over. |
total | number | Total scored cells included in the distribution (cells with null on the requested signal are excluded). |
buckets.prime | number | Cells scoring ≥ 80. |
buckets.strong | number | Cells scoring 60–79. |
buckets.solid | number | Cells scoring 40–59. |
buckets.watch | number | Cells scoring 20–39. |
buckets.elevated | number | Cells scoring < 20. |
The same endpoint backs distribution bars for any signal group — pass a different signal to render a development-pipeline mix, business-vitality mix, or composite mix without a separate route.
GET /api/score-trends
Historical score trends for a metro or specific H3 cell. Pass metro for a metro-wide aggregate trend or h3_index for a per-cell time-series with all eight signal-group sub-scores. Exactly one of the two is required.
The endpoint reads from the score_history table that the Railway scorer snapshots once per day, so a fresh row appears for every (h3_index, profile, snapshot_date) covered by that day’s scorer pass. Cells the scorer didn’t touch on a given day will be absent from the time-series for that date.
Query parameters
| Name | Type | Required | Default | Description |
|---|
metro | string | | | Metro slug for an aggregate metro-wide trend. |
h3_index | string | | | Specific H3 cell for a detailed per-cell trend with signal-group breakdowns. |
days | int | | 90 | Lookback period in days. |
Metro aggregate example
curl "https://axiomlocus.io/api/score-trends?metro=sf&days=30"
{ "metro": "sf", "days": 30, "trend": [{ "date": "2026-03-30", "avg_composite": 42, "cells": 20 }] }
| Field | Type | Description |
|---|
trend[].date | string | Snapshot date (YYYY-MM-DD) |
trend[].avg_composite | number | Average composite score across the metro for that date |
trend[].cells | number | Cells in the metro with a snapshot on that date |
Per-cell example
curl "https://axiomlocus.io/api/score-trends?h3_index=882a1072d3fffff&days=90"
{
"h3_index": "882a1072d3fffff",
"days": 90,
"snapshots": [
{
"snapshot_date": "2026-04-29",
"composite": 71,
"business_vitality": 68,
"population_momentum": 74,
"demographics": 65,
"economic_strength": 80,
"development_pipeline": 77,
"accessibility": 62,
"safety_environment": 70,
"amenity_demand": 64
}
]
}
| Field | Type | Description |
|---|
snapshots[].snapshot_date | string | Snapshot date (YYYY-MM-DD) |
snapshots[].composite | number | Cell composite score 0–100 on that date |
snapshots[].business_vitality … snapshots[].amenity_demand | number | null | Per-group sub-score 0–100; null if the group could not be computed for that snapshot |
GET /api/top-movers
Discover the cells whose composite score has changed the most over a 7-, 30-, 90-, or 180-day window. Each row reports the latest composite, the prior-window composite, and the delta between them, so you can surface neighborhoods that are heating up (or cooling off) without manually diffing snapshots from /api/score-trends.
Deltas are computed against the freshest snapshot in score_history at or before the lookback boundary, not a fixed calendar date. This means partial scorer coverage on any given day does not bias the leaderboard — cells without a baseline snapshot inside the window are simply excluded. The same RPC powers the metro-overview “movement” rail in the Explorer and the standalone /movers page in the dashboard.
Query parameters
| Name | Type | Required | Default | Description |
|---|
days | int | | 30 | Lookback window. Must be one of 7, 30, 90, or 180. |
direction | string | | up | up for biggest gainers, down for biggest decliners, or all to rank by absolute delta. |
metro | string | | | Metro slug filter (e.g. sf, nyc, boston). Omit for cross-metro results. |
limit | int | | 25 | Max results (1–100). |
Example
curl "https://axiomlocus.io/api/top-movers?days=30&direction=up&metro=boston&limit=10"
Response
{
"window_days": 30,
"direction": "up",
"metro": "boston",
"movers": [
{
"h3_index": "882a306665fffff",
"metro_slug": "boston",
"location_name": "Seaport District",
"latest_composite": 61,
"prior_composite": 38,
"delta": 23,
"latest_snapshot_date": "2026-04-29",
"prior_snapshot_date": "2026-03-30"
}
]
}
| Field | Type | Description |
|---|
window_days | int | Lookback window echoed from the request. |
direction | string | up, down, or all — echoed from the request. |
metro | string | null | Metro slug filter, or null when the call was cross-metro. |
movers | array | Cells ranked by composite delta. Empty when the window has no eligible snapshot pairs yet. |
movers[].h3_index | string | H3 cell index. |
movers[].metro_slug | string | Metro the cell belongs to. |
movers[].location_name | string | Neighborhood name. |
movers[].latest_composite | number | Composite score 0–100 from the most recent snapshot. |
movers[].prior_composite | number | Composite score 0–100 from the freshest snapshot at or before the lookback boundary. |
movers[].delta | number | latest_composite - prior_composite. Positive when the cell is improving, negative when declining. |
movers[].latest_snapshot_date | string | Date of the latest snapshot (YYYY-MM-DD). |
movers[].prior_snapshot_date | string | Date of the prior-side snapshot used as the baseline. |
Daily snapshots accumulate from the moment a cell is first scored, so newer metros may return an empty movers array on longer windows until enough history has built up. Use a shorter window (days=7) to surface activity earlier.
GET /api/schools
Nearby school quality ratings (1-10 scale).
Query parameters
| Name | Type | Required | Default | Description |
|---|
lat | float | ✓ | | Latitude. |
lng | float | ✓ | | Longitude. |
radius | float | | 2 | Radius in miles. |
limit | int | | 10 | Max results. |
Example
curl "https://axiomlocus.io/api/schools?lat=37.7749&lng=-122.4194&radius=1"
Response
{ "total": 5, "schools": [{ "name": "Lincoln Elementary", "rating": 8, "enrollment": 450 }] }
| Field | Type | Description |
|---|
schools[].rating | number | Quality score 1-10 |
schools[].enrollment | number | Student enrollment |
GET /api/life-sciences
Clinical trials and FDA enforcement data by state or sponsor.
Query parameters
| Name | Type | Required | Default | Description |
|---|
state | string | | | US state name. |
sponsor | string | | | Sponsor name (partial match). |
type | string | | all | Filter: trials, fda, or all. |
limit | int | | 50 | Max results. |
Example
curl "https://axiomlocus.io/api/life-sciences?state=Massachusetts&type=trials&limit=10"
Response
{ "trials": { "total": 10, "data": [{ "nct_id": "NCT05754281", "sponsor": "Joslin Diabetes Center" }] } }
| Field | Type | Description |
|---|
trials.data[].nct_id | string | ClinicalTrials.gov trial ID |
trials.data[].sponsor | string | Lead sponsor organization |
GET /api/zoning
Zoning district rules — allowed uses, height limits, FAR, setbacks.
Query parameters
| Name | Type | Required | Default | Description |
|---|
metro | string | ✓ | | Metro slug. |
code | string | | | Specific district code. |
category | string | | | Filter: residential, commercial, industrial, mixed_use. |
Example
curl "https://axiomlocus.io/api/zoning?metro=denver&category=commercial"
Response
{ "metro": "denver", "total": 8, "districts": [{ "district_code": "C-MX-5", "max_height_ft": 65, "max_far": 3.0 }] }
| Field | Type | Description |
|---|
districts[].district_code | string | Zoning designation code |
districts[].max_height_ft | number | Maximum building height in feet |
districts[].max_far | number | Floor area ratio |
GET /api/port-risk
Surface upstream port disruption affecting a Locus metro’s economic catchment. Twenty-two metros are mapped to their primary and secondary ports by trucking corridor (e.g. phoenix → Long Beach via I-10, atlanta → Savannah via I-16). When an inbound port shows elevated wait times in Overwatch, the row appears here with a risk_level of watch, elevated, or severe, derived from p90 wait, median wait, and trend direction. Returns an empty risks array when no upstream port is currently disrupted — the PortRiskBadge on cell-detail panels uses that signal to hide itself.
Use this to flag CRE locations whose tenants depend on container imports (industrial, logistics, big-box retail) before a soft port closure shows up in occupancy or rent comps.
Query parameters
| Name | Type | Required | Default | Description |
|---|
metro | string | ✓ | | Metro slug (e.g. houston, atlanta, chicago). |
Example
curl "https://axiomlocus.io/api/port-risk?metro=atlanta"
Response
{
"metro": "atlanta",
"risks": [
{
"port_slug": "savannah",
"port_name": "Port of Savannah",
"tier": "primary",
"trucking_km": 400,
"median_wait_hours": 18,
"p90_wait_hours": 84,
"vessel_count": 23,
"trend_direction": "worsening",
"trend_consecutive_periods": 3,
"risk_level": "severe",
"measured_at": "2026-04-29T00:00:00Z"
}
]
}
| Field | Type | Description |
|---|
metro | string | Metro slug echoed from the request. |
risks | array | Active port-risk rows, ordered by risk_level then p90_wait_hours (highest first). Empty when no upstream port is disrupted. |
risks[].port_slug | string | Stable port identifier (e.g. savannah, long-beach, ny-nj). |
risks[].port_name | string | Human-readable port name. |
risks[].tier | string | primary or secondary — the port’s role in the metro’s catchment. |
risks[].trucking_km | number | Driving distance from port to metro centroid (km). |
risks[].median_wait_hours | number | Median vessel wait time at the port over the measurement window. |
risks[].p90_wait_hours | number | 90th-percentile wait — the figure that drives most risk-level transitions. |
risks[].vessel_count | number | Vessels currently queued or anchored upstream. |
risks[].trend_direction | string | worsening, stable, or improving. |
risks[].trend_consecutive_periods | number | Periods the trend has held — used to confirm sustained disruption vs. a single-day spike. |
risks[].risk_level | string | watch, elevated, or severe. |
risks[].measured_at | string | ISO 8601 timestamp of the underlying Overwatch measurement. |
GET /api/export
Export scored location data as CSV or JSON. Requires Pro or Team plan (bulk_export entitlement).
Query parameters
| Name | Type | Required | Default | Description |
|---|
metro | string | | | Metro slug filter (e.g. sf, nyc). |
format | string | | csv | csv or json. |
saved | boolean | | false | When true, restricts the export to the caller’s saved cells (the cells in their monitored_locations portfolio). Composes with metro. |
limit | int | | 1000 | Max rows (5000 max). |
Portfolio export (saved=true)
Set saved=true to export only the cells the authenticated user has saved into their portfolio (their monitored_locations). This is the intended workflow for analysts who track a curated set of cells across metros and want a CSV/JSON snapshot of just those rows rather than a full-metro or platform-wide pull.
- The filter is applied as an
IN over the caller’s saved h3_index list, so the result respects ordering by composite (descending) and the limit cap.
- It composes with
metro — combining saved=true&metro=sf returns only saved cells inside SF.
- If the user has no saved cells, the endpoint short-circuits to an empty payload (
200 OK) without scanning cell_scores. CSV responses return an empty body; JSON responses return { "total": 0, "data": [] }.
- The download filename is tagged
saved (e.g. axiom-locus-saved-2026-04-29.csv) instead of the metro slug or all, so portfolio exports are easy to identify on disk.
Examples
Export all SF cells as CSV:
curl "https://axiomlocus.io/api/export?metro=sf&format=csv" -H "X-API-Key: al_xxx"
Export the caller’s saved cells (portfolio) as CSV:
curl "https://axiomlocus.io/api/export?saved=true&format=csv" -H "X-API-Key: al_xxx"
Export the caller’s saved cells inside one metro as JSON:
curl "https://axiomlocus.io/api/export?saved=true&metro=sf&format=json" -H "X-API-Key: al_xxx"
Response
h3_index,metro_slug,location_name,composite,...
88283082e7fffff,sf,Mission District,74,...
GET /api/bids
Search federal and government procurement opportunities sourced from SAM.gov. Filter by state, keyword, or NAICS code to find relevant contracts.
Query parameters
| Name | Type | Required | Default | Description |
|---|
state | string | | | Two-letter state code (e.g. TX, CA). |
keyword | string | | | Full-text search across title and description. |
naics | string | | | NAICS code filter (e.g. 236220 for commercial construction). |
type | string | | | Bid type (partial match). |
active | string | | true | Set to false to include expired bids. |
limit | int | | 50 | Max results (max 200). |
Example
curl "https://axiomlocus.io/api/bids?state=TX&keyword=construction&limit=10"
Response
{
"state": "TX",
"keyword": "construction",
"count": 10,
"bids": [
{
"notice_id": "SAM-2026-001234",
"title": "Renovation of Federal Building, Houston TX",
"type": "Solicitation",
"agency": "General Services Administration",
"posted_date": "2026-04-10",
"response_deadline": "2026-05-15",
"description": "Full interior renovation of the federal courthouse...",
"naics_code": "236220",
"place_of_performance_city": "Houston",
"place_of_performance_state": "TX",
"estimated_value": 4500000,
"url": "https://sam.gov/opp/abc123",
"set_aside_type": "Small Business"
}
]
}
| Field | Type | Description |
|---|
count | int | Number of matching bids returned. |
bids[].notice_id | string | SAM.gov notice identifier. |
bids[].response_deadline | string | Deadline for bid submissions. |
bids[].naics_code | string | NAICS industry classification code. |
bids[].estimated_value | number | Estimated contract value in USD. |
bids[].set_aside_type | string | Set-aside designation (e.g. Small Business, 8(a), HUBZone). |