FlowFabric provides REST endpoints for querying hydrologic datasets with production-grade performance. Get 44 years of NWM reanalysis in <8 seconds, or real-time forecast data in <500ms.
| Feature | Benefit |
|---|---|
| Arrow IPC format | 50–70% smaller responses, near-instant deserialization |
| Sub-second queries | Most forecast queries <1 s; 44-year reanalysis <10 s |
| Cost estimation | ?estimate=true previews row and byte count before you commit |
| Async exports | Large queries return a Parquet file link instead of streaming |
| Per-user metering | Live usage tracking via GET /v1/me/usage |
| Reach discovery | Find feature_id values by coordinates or bounding box |
https://flowfabric-api.lynker-spatial.com
Local development: http://127.0.0.1:8000
All data endpoints require a Bearer token.
Click Sign in with SSO on the /docs page. This starts an OAuth flow and sets a session cookie automatically for interactive use.
curl -H "Authorization: Bearer $TOKEN" \
https://flowfabric-api.lynker-spatial.com/v1/datasets
API keys can be created at POST /v1/me/keys and used exactly like a Bearer token:
curl -H "Authorization: Bearer ffk_<your-key>" \
https://flowfabric-api.lynker-spatial.com/v1/datasets
Keys are scoped to your tier and datasets. The full key is shown once at creation — store it securely.
{
"error": {
"code": "unauthorized",
"message": "Invalid or expired token"
}
}
| Tier | Requests/min | Max feature IDs | Monthly data |
|---|---|---|---|
| free | 20 | 10,000 | 3 GB |
| standard | 120 | 500,000 | Unlimited |
| pro | 600 | 2,000,000 | Unlimited |
| enterprise | 3,000 | 3,000,000 | Unlimited |
Check your current tier and remaining quota at GET /v1/me.
| Method | Path | Purpose |
|---|---|---|
| GET | /healthz |
Health check |
| GET | /v1/datasets |
List available datasets |
| GET | /v1/datasets/{dataset_id} |
Dataset metadata |
| GET | /v1/datasets/{dataset_id}/runs/latest |
Latest run metadata |
| GET | /v1/datasets/{dataset_id}/runs/{issue_time} |
Run metadata by issue time |
| POST | /v1/features/nearest |
Find reaches near a point |
| POST | /v1/features/bbox |
Find reaches in a bounding box |
| POST | /v1/{dataset_id}/streamflow |
Query streamflow time-series |
| POST | /v1/ratings |
Query stage-discharge rating curves |
| POST | /v1/stage |
Translate streamflow to water surface elevation |
| POST | /v1/inundation-ids |
Get inundated grid cell IDs |
| GET | /v1/me |
Your profile and tier limits |
| GET | /v1/me/usage |
Your usage summary |
| GET | /v1/me/queries |
Your recent query log |
| POST | /v1/me/keys |
Create an API key |
| GET | /v1/me/keys |
List your API keys |
| DELETE | /v1/me/keys/{key_id} |
Revoke an API key |
GET /healthz
{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2026-04-28T12:00:00.000000Z"
}
GET /v1/datasets — list all available datasets.
GET /v1/datasets/{dataset_id} — metadata for a single dataset, including spatial coverage, temporal range, and variable list.
GET /v1/datasets/{dataset_id}/runs/latest — metadata for the most recently available forecast run.
GET /v1/datasets/{dataset_id}/runs/{issue_time} — metadata for a specific run (issue time in YYYYMMDDHH format).
Use these endpoints first to find the feature_id values for your area of interest. Those IDs are then passed to the data endpoints.
POST /v1/features/nearest
Returns the N stream reaches closest to a point.
Request body
{
"lon": -105.0,
"lat": 40.6,
"n": 3,
"max_distance_m": 10000,
"include_geometry": false
}
| Field | Type | Default | Description |
|---|---|---|---|
lon |
float | required | Longitude (WGS84) |
lat |
float | required | Latitude (WGS84) |
n |
int | 1 | Number of nearest reaches (max 100) |
max_distance_m |
float | 5000 | Search radius in metres (max 100,000) |
include_geometry |
bool | false | Include GeoJSON geometry and distance in response |
Response (200 OK)
{
"feature_ids": ["2900009", "2900015", "2900021"],
"count": 3
}
POST /v1/features/bbox
Returns all stream reaches whose centroids fall within a bounding box.
Request body
{
"bbox": [-105.1, 40.5, -104.9, 40.7],
"stream_order_min": 2,
"max_features": 500,
"include_geometry": false
}
| Field | Type | Default | Description |
|---|---|---|---|
bbox |
[float×4] | required | [minLon, minLat, maxLon, maxLat] in WGS84 |
stream_order_min |
int | — | Only return reaches with Strahler order ≥ N |
max_features |
int | 500 | Maximum features to return (capped at 5,000) |
include_geometry |
bool | false | Include GeoJSON geometry in response |
Constraint: Bounding box cannot exceed 10° in either dimension. Use stream_order_min to filter large areas.
Response (200 OK)
{
"feature_ids": ["2900009", "2900015"],
"count": 2
}
POST /v1/{dataset_id}/streamflow
Returns discharge (m³/s) time-series for one or more stream reaches. Supports both NWM forecast runs and reanalysis datasets.
Add ?estimate=true to preview row/byte count without transferring data (free, fast).
{
"query_mode": "run",
"issue_time": "latest",
"scope": "features",
"feature_ids": ["2900009", "2900015"],
"format": "arrow",
"mode": "sync"
}
| Field | Type | Default | Description |
|---|---|---|---|
query_mode |
string | required | "run" for forecast datasets |
issue_time |
string | required | YYYYMMDDHH or "latest" |
lead_start |
int | — | Lead hour start (optional) |
lead_end |
int | — | Lead hour end (optional, requires lead_start) |
valid_time_start |
string | — | ISO8601 UTC alternative to lead times |
valid_time_end |
string | — | ISO8601 UTC alternative to lead times |
scope |
string | required | "features", "all" |
feature_ids |
[string] | — | Required when scope="features" |
format |
string | "arrow" |
"arrow", "json", "parquet" |
mode |
string | "sync" |
"sync" or "export" |
{
"query_mode": "absolute",
"start_time": "2023-01-01T00:00:00Z",
"end_time": "2023-12-31T23:00:00Z",
"scope": "features",
"feature_ids": ["2900009"],
"format": "arrow",
"mode": "sync"
}
| Field | Type | Default | Description |
|---|---|---|---|
query_mode |
string | required | "absolute" for reanalysis datasets |
start_time |
string | required | ISO8601 UTC |
end_time |
string | required | ISO8601 UTC |
scope |
string | required | "features", "all" |
feature_ids |
[string] | — | Required when scope="features" |
format |
string | "arrow" |
"arrow", "json", "parquet" |
mode |
string | "sync" |
"sync" or "export" |
?estimate=true){
"estimated_rows": 38,
"estimated_bytes": 1520,
"recommended_mode": "sync",
"would_exceed_sync_limits": false
}
application/vnd.apache.arrow.streamResponse headers:
- X-Request-ID — unique request identifier
- X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset — rate limit status
When mode="export" the API returns a JSON envelope with a pre-signed download link:
{
"export_id": "export-abc123",
"download_url": "https://...",
"expires_at": "2026-05-05T12:00:00Z",
"estimated_size_gb": 2.4
}
Reading Arrow IPC in Python
import httpx
import pyarrow as pa
response = httpx.post(
"https://flowfabric-api.lynker-spatial.com/v1/nwm-forecast/streamflow",
headers={"Authorization": f"Bearer {token}"},
json={
"query_mode": "run",
"issue_time": "latest",
"scope": "features",
"feature_ids": ["2900009", "2900015"]
}
)
reader = pa.ipc.open_stream(response.content)
df = reader.read_all().to_pandas()
print(df)
# feature_id time streamflow
# 0 2900009 2026-04-28 ... 125.4
# 1 2900015 2026-04-28 ... 2341.1
Reading Arrow IPC in R
library(arrow)
resp <- httr::POST(
"https://flowfabric-api.lynker-spatial.com/v1/nwm-forecast/streamflow",
httr::add_headers(Authorization = paste("Bearer", token)),
body = list(query_mode="run", issue_time="latest",
scope="features", feature_ids=list("2900009")),
encode = "json"
)
df <- as.data.frame(arrow::read_ipc_stream(httr::content(resp, "raw")))
POST /v1/ratings
Returns stage-discharge rating curves — a lookup table of (discharge m³/s → stage m) pairs for each requested reach. Rating curves are required to convert streamflow values into water surface elevation.
Request body
{
"feature_ids": ["2900009", "2900015"],
"type": "rem",
"format": "arrow"
}
| Field | Type | Default | Description |
|---|---|---|---|
feature_ids |
[string] | required | NHM reach IDs |
type |
string | "rem" |
Rating curve type: "rem" or "ahps" |
format |
string | "arrow" |
"arrow" or "json" |
Response columns
| Column | Type | Description |
|---|---|---|
feature_id |
int64 | Stream reach identifier |
stage |
float64 | Water surface elevation (m) |
default_discharge_cms |
float64 | Discharge at this stage (m³/s) |
Performance: Typically < 1 second regardless of feature count, due to partition-level filtering.
POST /v1/stage
Converts streamflow forecasts into water surface elevation for each reach in a single request. Internally chains a streamflow query with a rating curve lookup — you don't need to make both calls separately.
Request body
{
"dataset_id": "nwm/analysis",
"issue_time": "latest",
"feature_ids": ["2900009", "2900015"],
"ratings_type": "rem",
"format": "json"
}
| Field | Type | Default | Description |
|---|---|---|---|
dataset_id |
string | required | Dataset to pull streamflow from |
issue_time |
string | required | YYYYMMDDHH or "latest" |
feature_ids |
[string] | required | NHM reach IDs |
start_time |
string | — | Optional ISO8601 UTC start filter |
end_time |
string | — | Optional ISO8601 UTC end filter |
ratings_type |
string | "rem" |
"rem" or "ahps" |
format |
string | "arrow" |
"arrow" or "json" |
Response columns
| Column | Type | Description |
|---|---|---|
time |
timestamp | Valid time |
feature_id |
int64 | Stream reach identifier |
streamflow |
float64 | Discharge (m³/s) |
stage |
float64 | Water surface elevation (m) — NaN if no rating curve |
POST /v1/inundation-ids
Returns the set of FIM grid cell IDs that are inundated given a streamflow snapshot. Use these IDs to render flood extent maps on a pre-built flood index.
Request body
{
"dataset_id": "nwm/analysis",
"issue_time": "2026042806",
"ratings_type": "rem",
"tolerance": 0.05,
"min_flow": 1.0
}
| Field | Type | Default | Description |
|---|---|---|---|
dataset_id |
string | required | Dataset to pull streamflow from |
issue_time |
string | required | YYYYMMDDHH |
ratings_type |
string | "rem" |
"rem" or "ahps" |
tolerance |
float | 0.05 | Matching tolerance (0.05 = ±5%) |
min_flow |
float | 1.0 | Minimum flow threshold (m³/s); reaches below this are excluded |
GET /v1/me
Returns your identity, tier, and access context. No metering reads — always fast.
{
"email": "you@example.com",
"tier": "standard",
"token_type": "jwt",
"datasets": ["*"],
"scopes": ["datasets:read"],
"tier_limits": {
"rate_limit_per_minute": 120,
"max_feature_ids": 500000,
"max_sync_rows": 5000000,
"max_sync_bytes": 500000000
}
}
GET /v1/me/usage?period=30d
Aggregate usage for the current account, broken down by period.
Period values: 7d, 30d, 90d, all (default 30d).
{
"email": "you@example.com",
"tier": "free",
"period": "30d",
"total_queries": 42,
"total_bytes": 1500000,
"total_bytes_gb": 0.0015,
"monthly_bytes_used": 1500000,
"monthly_bytes_remaining": 2998500000,
"tier_limits": { ... }
}
monthly_bytes_used / monthly_bytes_remaining are only included for tiers with a monthly cap (currently free).
GET /v1/me/queries?limit=20&offset=0&period=30d
Paginated list of your recent API calls with latency, bytes, and endpoint.
POST /v1/me/keys — create a new API key. The full key (ffk_...) is returned once only.
{ "label": "my-dashboard-key" }
GET /v1/me/keys — list all keys (key prefix shown, never the full value).
DELETE /v1/me/keys/{key_id} — revoke a key (soft-delete, takes effect within 60 seconds).
All errors follow this schema:
{
"error": {
"code": "error_code",
"message": "Human-readable description"
}
}
| Code | HTTP Status | Meaning |
|---|---|---|
unauthorized |
401 | Missing or invalid token |
forbidden |
403 | Your tier/datasets don't allow this |
bad_request |
400 | Invalid request parameters |
feature_id_limit_exceeded |
400 | Too many feature IDs for your tier |
not_found |
404 | Dataset or run not found |
rate_limit_exceeded |
429 | Too many requests — check Retry-After header |
monthly_quota_exceeded |
429 | Monthly data cap reached |
internal_error |
500 | Server error |
feature_disabled |
503 | Feature not enabled on this deployment |
Binary columnar format — the fastest option for machine consumption.
| Format | Relative size | Parse time (Python) |
|---|---|---|
| Arrow IPC | 1× (smallest) | ~1 ms |
| Parquet | ~1.2× | ~5 ms |
| JSON | ~2.5–3× | ~50 ms |
All Arrow IPC responses carry Content-Type: application/vnd.apache.arrow.stream.
JavaScript
import * as arrow from "apache-arrow";
const buffer = await response.arrayBuffer();
const table = arrow.tableFromIPC(buffer);
Pass "format": "json" in the request body. Returns a standard JSON envelope:
{
"data": [ { "time": "2026-04-28T06:00:00", "feature_id": 2900009, "streamflow": 125.4 } ],
"count": 1
}