← Back to API Docs

FlowFabric API Reference

Overview

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.

Key Features

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

Base URL

https://flowfabric-api.lynker-spatial.com

Local development: http://127.0.0.1:8000


Authentication

All data endpoints require a Bearer token.

Option 1 — SSO (browser)

Click Sign in with SSO on the /docs page. This starts an OAuth flow and sets a session cookie automatically for interactive use.

Option 2 — API token (programmatic)

curl -H "Authorization: Bearer $TOKEN" \
  https://flowfabric-api.lynker-spatial.com/v1/datasets

Option 3 — API key (self-service)

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.

Auth error

{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or expired token"
  }
}

Tiers & Limits

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.


Endpoint Index

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

Core Endpoints

Health Check

GET /healthz

{
  "status": "healthy",
  "version": "1.0.0",
  "timestamp": "2026-04-28T12:00:00.000000Z"
}

Datasets

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).


Feature Discovery

Use these endpoints first to find the feature_id values for your area of interest. Those IDs are then passed to the data endpoints.

Nearest Reach

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
}

Reaches in Bounding Box

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
}

Streamflow

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).

Forecast (run-based)

{
  "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"

Reanalysis (absolute time range)

{
  "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 response (?estimate=true)

{
  "estimated_rows": 38,
  "estimated_bytes": 1520,
  "recommended_mode": "sync",
  "would_exceed_sync_limits": false
}

Sync streaming response (200 OK)

Response headers: - X-Request-ID — unique request identifier - X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset — rate limit status

Export response

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")))

Rating Curves

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.


Stage (Streamflow → Elevation)

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

Inundation IDs

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

My Account

Profile

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
  }
}

Usage Summary

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).

Query Log

GET /v1/me/queries?limit=20&offset=0&period=30d

Paginated list of your recent API calls with latency, bytes, and endpoint.

API Keys

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).


Error Response Format

All errors follow this schema:

{
  "error": {
    "code": "error_code",
    "message": "Human-readable description"
  }
}

Common Error Codes

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

Data Formats

Arrow IPC (default)

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);

JSON

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
}

Support