Continuuiti’s REST APIs give you programmatic access to geocoding, climate risk, land cover, biodiversity, site terrain, pluvial and flash flood simulation, riverine and coastal flood fields, water stress, damage estimation, and hazard history for any geographic coordinate worldwide. Base URL: https://api.continuuiti.com/api/v1/. This page is the complete developer reference: every endpoint, every parameter, every response field, with cURL, Python, and Node code examples for every API.
Getting Started
Get your API key
Every Continuuiti Tools account includes a personal API key. To get yours:
- Create an account at tools.continuuiti.com with your business email.
- Open Platform API in the sidebar. Your key (
sk_live_...) is generated automatically, so copy it from there. - Pass it on every request in the
X-API-Keyheader.
You can regenerate the key at any time from the same page. The old key stops working immediately.
402 with your current balance, and an access request is logged for our team automatically. Reading your results is always free. To see your balance in each module, call GET /api/v1/credits/.Every Continuuiti API request goes to https://api.continuuiti.com/api/v1/ with an X-API-Key header. Below is the simplest possible request — geocoding an address — that you can run in your terminal right now after generating an API key.
curl -X POST https://api.continuuiti.com/api/v1/geocode/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"address": "350 Fifth Avenue, New York, NY"}'
You’ll get a JSON response with the matched coordinates, formatted address, and a structured quality envelope describing how confident the match is:
{
"id": 1,
"original_address": "350 Fifth Avenue, New York, NY",
"formatted_address": "350 5th Ave, New York, NY 10118, USA",
"latitude": "40.7484405",
"longitude": "-73.9856644",
"quality": {"score": 1.0, "status": "PASSED", "issues": [], "match_type": "building"},
"created_at": "2026-04-27T10:19:05Z"
}
Pick the API you need: Climate Risk, Geocoder, LULC, Biodiversity, CDEM, Pluvial & Flash Flood, Flood Field, Water Stress, Damage Estimation, or Hazard History. The cross-cutting sections below (Authentication, Batch & Async, Sandbox, Errors, Rate Limits) apply to every API.
Authentication
Every Continuuiti API request requires an API key passed in the X-API-Key HTTP header. There is no OAuth flow, no Bearer token, and no query-parameter authentication — only the header. Generate your API key from the Continuuiti API dashboard.
Header pattern
Add a single header to every request. The same key authenticates against every endpoint across every Continuuiti API.
curl https://api.continuuiti.com/api/v1/exports/geocoding/reports/ \
-H "X-API-Key: YOUR_API_KEY"
import requests
headers = {"X-API-Key": "YOUR_API_KEY"}
response = requests.get(
"https://api.continuuiti.com/api/v1/exports/geocoding/reports/",
headers=headers,
)
data = response.json()
const response = await fetch(
"https://api.continuuiti.com/api/v1/exports/geocoding/reports/",
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await response.json();
Authentication errors
Requests without an API key, or with an invalid or revoked key, return 401 Unauthorized:
HTTP/2 401
Content-Type: application/json
{"error": "Invalid API key", "code": "auth_invalid"}
Response fields
Responses contain the documented fields plus a few platform extras: report_id and report_url (a deep link to the report in your dashboard), tools_batch_id and batch_url for batches, and credits_remaining after each submission. Results endpoints return 409 while a job is still running.
Check your credits
GET /api/v1/credits/ returns your credit balance in every module you can call through the API. The request is free and changes nothing.
curl -H "X-API-Key: YOUR_API_KEY" https://api.continuuiti.com/api/v1/credits/
Response (200 OK)
{
"credits": {
"geocode": 954,
"lulc": 897,
"climate-risk": 842,
"flood-field": 969,
"cdem": 898,
"pluvial": 980,
"water-stress": 799,
"damage": 927,
"hazard-history": 600
}
}
- One key per module, named as in the module’s route. Every key is always present; 0 means you hold no credits in that module.
- Credits in one module cannot be spent in another, so the response has no total.
- Biodiversity runs inside every LULC+ analysis and spends
lulccredits. - A missing or invalid key returns
401.
Batch & Async Pattern
Several APIs process work asynchronously because each location involves multi-second computation. LULC, Climate Risk, CDEM, Pluvial & Flash Flood, and Flood Field are always asynchronous; the Geocoder (batches over five addresses) and Hazard History (batches) also use this pattern. Water Stress answers synchronously, for a single site and for a batch. They share an identical flow: submit → poll → fetch results. Biodiversity screening is bundled into LULC, so it has no separate job.
The flow
- POST to
/{api}/batch/analyze/with your locations array. Returns202 Acceptedwith abatch_idimmediately — processing has not started yet. - GET
/{api}/batch/{batch_id}/repeatedly to poll progress. Status moves through PROCESSING and ends at COMPLETED, PARTIAL, or FAILED. - GET
/{api}/batch/{batch_id}/results/?page=1&page_size=100to fetch paginated results once the batch reaches a terminal state.
Status values
| Status | Meaning |
|---|---|
PENDING |
Batch created, processing not yet started |
PROCESSING |
Jobs actively running across worker pool |
COMPLETED |
All jobs reached a terminal state successfully |
PARTIAL |
Some jobs failed permanently; some succeeded. Results endpoint returns both |
FAILED |
All jobs failed. Inspect error on the batch record |
Per-module variations: single-location jobs never report PARTIAL (a partly-successful single job is stored as COMPLETED), except CDEM, Flood Field and Pluvial & Flash Flood jobs, which end PARTIAL when one layer or storm carries data and another does not; those three also report QUEUED between PENDING and PROCESSING. LULC batches report RUNNING instead of PROCESSING; Geocoder batches have no PARTIAL.
Automatic retry behavior
Async APIs retry transient failures automatically before marking a job as failed. The status response surfaces this so your polling logic doesn’t see a transient FAILED that becomes COMPLETED on retry.
| Field | Where it appears | Meaning |
|---|---|---|
retry_info |
Job status | null on first attempt; when present, contains {"attempt": N, "max_attempts": M} indicating the job is being auto-retried |
retrying |
Batch progress | Count of jobs that failed but have retries remaining (auto-recovering) |
failed |
Batch progress | Count of permanently failed jobs (all retries exhausted) |
While a job is being retried, its status field shows PENDING rather than FAILED, so client polling logic can simply continue.
Polling cadence
Poll every 5 to 10 seconds. Faster polling consumes API quota with no benefit; slower polling delays your application’s response to completion. For long-running batches (LULC at 5,000 locations runs 30+ minutes), consider exponential backoff after the first minute.
import requests
import time
API_BASE = "https://api.continuuiti.com/api/v1"
HEADERS = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}
# 1. Submit batch
response = requests.post(
f"{API_BASE}/climate-risk/batch/analyze/",
headers=HEADERS,
json={
"locations": [
{"lat": 28.6139, "lon": 77.2090, "label": "site-01"},
{"lat": 19.0760, "lon": 72.8777, "label": "site-02"},
],
},
)
batch_id = response.json()["batch_id"]
print(f"Submitted batch: {batch_id}")
# 2. Poll until terminal
while True:
status_response = requests.get(f"{API_BASE}/climate-risk/batch/{batch_id}/", headers=HEADERS)
state = status_response.json()
print(f"Status: {state['status']} | Progress: {state['progress']['percent_complete']}%")
if state["status"] in ("COMPLETED", "PARTIAL", "FAILED"):
break
time.sleep(10)
# 3. Fetch results (paginated)
page = 1
while True:
results_response = requests.get(
f"{API_BASE}/climate-risk/batch/{batch_id}/results/",
headers=HEADERS,
params={"page": page, "page_size": 100},
)
payload = results_response.json()
for item in payload["results"]:
if item["status"] == "COMPLETED":
print(f"{item['label']}: {item['result']['composite_score']}")
else:
print(f"{item['label']}: {item['status']} ({item['error']})")
if page >= payload["total_pages"]:
break
page += 1
Retrieving Your Results
Alongside the per-job results endpoints, every module exposes a uniform read-only export API for listing and fetching your stored reports and batches.
/api/v1/exports/{module}/reports/List your reports for a module (paginated and filterable).
/api/v1/exports/{module}/reports/{id}/Fetch one full report with its results.
/api/v1/exports/{module}/batches/List your batches for a module.
/api/v1/exports/{module}/batches/{id}/Fetch one batch and its reports.
{module} is one of lulc, climate-risk, geocoding, damage-curves, hazard-history, cdem, flood-field, water-stress, or pluvial (pluvial also serves /batches/{id}/csv/).
Query parameters: status, page, page_size (max 200), created_after, created_before, and ordering.
Sandbox / Dry-Run Mode
Every endpoint that consumes computation supports a sandbox mode that returns deterministic mock data without burning API quota. Use it for integration tests, CI pipelines, frontend development, or any time you need to exercise the API contract without hitting the real compute path. Sandbox mode is the recommended way to validate your integration before going to production.
Sandbox runs are free. No credits are consumed, even at zero balance, and each run is labelled with a Sandbox badge in your dashboard so it never mixes with real results.
How to enable sandbox mode
For POST endpoints, add "dry_run": true to the request body. For GET endpoints (like Hazard History queries), add ?dry_run=true to the query string. The same input always returns the same output, so you can write deterministic snapshot tests against sandbox responses.
curl -X POST https://api.continuuiti.com/api/v1/geocode/batch/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"addresses": [{"address": "350 Fifth Avenue, New York, NY"}],
"dry_run": true
}'
import requests
API_BASE = "https://api.continuuiti.com/api/v1"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
response = requests.post(
f"{API_BASE}/climate-risk/batch/analyze/",
headers=HEADERS,
json={
"locations": [{"lat": 28.6139, "lon": 77.2090, "label": "site-01"}],
"dry_run": True,
},
)
batch = response.json() # a dry-run batch is already COMPLETED on submit
results = requests.get(
f"{API_BASE}/climate-risk/batch/{batch['batch_id']}/results/",
headers=HEADERS,
).json()
hazards = results["results"][0]["result"]["hazards"]
# Every hazard in a climate risk dry run is tagged in its context
assert all(h["context"]["dry_run"] for h in hazards.values())
const response = await fetch(
"https://api.continuuiti.com/api/v1/hazard-history/floods/query/?lat=29.76&lon=-95.37&dry_run=true",
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await response.json();
console.log(data.metadata); // includes [DRY RUN] prefix
What sandbox returns
Sandbox responses match the production response shape exactly — same fields, same types, same nesting. Only the values are deterministic mocks. Each sandbox result is marked so you cannot accidentally treat it as real data. In a climate risk dry run, every hazard carries "dry_run": true in its context:
{
"hazards": {
"heat_wave": {
"context": {"dry_run": true},
...
},
...
}
}
Errors & Status Codes
Continuuiti APIs follow standard HTTP status codes. The response body is always JSON. Error responses include an error field, which usually holds a human-readable message. In a 402 it holds the code insufficient_credits, and the readable text is in the message field.
HTTP status codes
| Code | Meaning | What to do |
|---|---|---|
200 OK |
Successful sync request | Parse the response body |
201 Created |
Resource created (geocoding, damage estimation) | Parse response; id field is the new resource ID |
202 Accepted |
Async job or batch submitted; processing not started | Begin polling the status endpoint with the returned job_id or batch_id |
400 Bad Request |
Malformed request: missing required field, invalid coordinate, oversized batch (>5,000 items), invalid enum value | Inspect the error field; fix the request and retry. Do not retry blindly |
401 Unauthorized |
API key missing, invalid, or revoked | Verify X-API-Key header; check key in dashboard |
402 Payment Required |
Insufficient credits for the request | Top up credits, then retry. You are not charged for the refused request. The body names the pool that ran out (pool) and its key in GET /api/v1/credits/ (credits_key), and gives credits_required and credits_available. It also links the balance endpoint (credits_url) and your account settings page (request_access_url). See the example below. |
404 Not Found |
Resource not found (job_id, batch_id, geocode id, occupancy code, country code) | Verify the ID is correct; for geocoding, address may have failed to resolve |
405 Method Not Allowed |
Wrong HTTP method for the endpoint | Use the documented method (POST to submit, GET to poll or fetch) |
409 Conflict |
Results requested while the job is still running | Keep polling the jobs endpoint; fetch results once COMPLETED |
429 Too Many Requests |
Rate limit exceeded | Back off and retry. Reduce concurrency |
503 Service Unavailable |
Transient upstream issue (rare) | Retry with exponential backoff. The async retry layer handles most transients automatically |
Error response shape
{
"error": "Invalid request: 'lat' must be between -90 and 90"
}
Insufficient credits (402 Payment Required)
{
"error": "insufficient_credits",
"credits_required": 601,
"credits_available": 600,
"has_pending_request": true,
"pool": "hazard_history",
"credits_key": "hazard-history",
"credits_url": "https://api.continuuiti.com/api/v1/credits/",
"request_access_url": "https://tools.continuuiti.com/accounts/settings/",
"message": "You need 601 credits but only have 600."
}
Rate Limits
The default limits are 60 requests per minute and 500 requests per hour, per account. When you exceed them, the API returns 429 with a Retry-After header indicating how long to wait.
These limits suit typical integration workloads, including batch submissions of up to 5,000 locations per request. Burst patterns and sustained high-volume usage are supported on commercial plans.
For sustained workloads beyond the defaults, custom rate limits, dedicated capacity, or SLA-backed usage agreements, book a demo to discuss your requirements.
Climate Risk API
The Continuuiti Climate Risk API assesses 12 physical climate hazards across three emissions scenarios (SSP1-2.6, SSP2-4.5 and SSP5-8.5) and four time horizons (baseline, 2030, 2040, 2050) for any geographic coordinate. Every response covers all three scenarios and all four horizons. Returns hazard ratings, composite risk score with confidence, and a top-risks summary aligned with TCFD physical risk disclosure categories.
12 Climate Hazards
Every climate risk assessment evaluates all 12 hazards.
| Hazard | Category | What it measures | Typical units |
|---|---|---|---|
heat_wave |
Temperature | Additional days a year above a heat threshold set 5°C over the baseline mean daily maximum temperature | days/year |
cold_stress |
Temperature | Cold stress days a year, estimated from mean daily minimum temperature | days/year |
temperature_change |
Temperature | Annual mean temperature shift from baseline | °C |
drought |
Precipitation | Severe drought months a year, from the change in mean precipitation | months/year |
extreme_rainfall |
Precipitation | Days exceeding the 99th percentile precipitation | days/year |
precipitation_change |
Precipitation | Annual precipitation shift from baseline | % |
severe_storm |
Compound | Storm days a year from surface wind extremes; the rating also reflects tropical cyclone exposure | days/year |
wildfire |
Compound | Fire weather days a year from daily maximum temperature; rated Low on built, bare, water and snow cover | days/year |
landslide |
Compound | Landslide trigger days a year: heavy rainfall days weighted by terrain slope | days/year |
river_flood |
Hydrological | Change in high-discharge days a year from heavier rainfall, scaled by the site’s floodplain class on the JRC river flood map. A rainfall proxy: no river discharge is modelled | days/year |
sea_level_rise |
Hydrological | Freeboard: site elevation above projected sea level plus a 1.5 m tidal buffer | meters |
water_stress |
Hydrological | Basin-level water supply and demand stress | score (0 to 5) |
Risk Levels
Each hazard returns a categorical rating on a 5-tier scale. The composite score maps those ratings to scores of 1 to 5 and takes a weighted aggregate across all 12 hazards.
| Rating | Score | Interpretation |
|---|---|---|
| Low | 1 | Minimal climate exposure; no adaptation action required |
| Moderate | 2 | Some exposure; routine monitoring |
| High | 3 | Significant exposure; adaptation planning recommended |
| Severe | 4 | Major exposure; adaptation investment likely required |
| Extreme | 5 | Critical exposure; site-level engineering response warranted |
Scenarios
Every climate risk response covers three Shared Socioeconomic Pathways:
- SSP1-2.6 (low emissions): strong mitigation and rapid decarbonization, holding warming to about 1.8°C by 2100.
- SSP2-4.5 (moderate emissions): middle-of-the-road pathway, consistent with current policy pledges. Standard central planning assumption for risk assessment and regulatory disclosure.
- SSP5-8.5 (high emissions): fossil-fueled development with limited climate policy. Standard worst-case stress-test scenario for TCFD and IFRS S2 disclosure.
Time Horizons
Four time horizons are available. Each represents a 20-year averaging window centered on the labeled year (e.g., “2050” = 2040–2059 average), capturing the underlying climate trend rather than year-to-year fluctuation.
| Horizon | Period | Use case |
|---|---|---|
baseline |
1980–2010 historical reference | “Current climate” anchor against which futures are compared |
2030 |
2020–2039 average | Near-term planning horizon |
2040 |
2030–2049 average | Medium-term planning horizon |
2050 |
2040–2059 average | Standard TCFD/IFRS S2 long-term horizon |
Endpoints
/api/v1/climate-risk/analyze/Submit a single-location climate risk job. Returns 201 Created with a job_id immediately; processing runs asynchronously.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
lat |
number | Yes | — | Latitude (-90 to 90) |
lon |
number | Yes | — | Longitude (-180 to 180) |
label |
string | No | none | Your name for the location, stored with the report in your dashboard |
dry_run |
boolean | No | false |
Return a deterministic mock without consuming quota. See Sandbox |
Every assessment returns all three scenarios (ssp126, ssp245, ssp585) and all four horizons (baseline, 2030, 2040, 2050) for all 12 hazards.
Response (201 Created)
{
"job_id": "climate_a1b2c3d4e5f6",
"status": "RUNNING",
"error": null,
"report_id": "6f1d2c3b-8a9e-4b7c-9d0e-1f2a3b4c5d6e",
"report_url": "/climate-risk/reports/6f1d2c3b-8a9e-4b7c-9d0e-1f2a3b4c5d6e/"
}
Errors
| Status | Reason |
|---|---|
400 |
Invalid or missing lat/lon, invalid JSON |
/api/v1/climate-risk/jobs/{job_id}/Get the status of a climate risk job.
Response (200 OK)
{
"job_id": "climate_a1b2c3d4e5f6",
"status": "RUNNING",
"error": null,
"report_id": "6f1d2c3b-8a9e-4b7c-9d0e-1f2a3b4c5d6e",
"report_url": "/climate-risk/reports/6f1d2c3b-8a9e-4b7c-9d0e-1f2a3b4c5d6e/"
}
Status values: RUNNING, then COMPLETED or FAILED. A job with partial results also reads COMPLETED here; the results body carries "status": "PARTIAL" (see below).
Errors
| Status | Reason |
|---|---|
404 |
Job not found |
/api/v1/climate-risk/results/{job_id}/Get the full results of a completed climate risk assessment. While the job is still running, this returns 409 with a short status preview ({"error": "Job not completed", "status": "RUNNING"}); poll the jobs endpoint until the job is COMPLETED, then fetch results.
Response (200 OK) — truncated for readability
{
"job_id": "climate_a1b2c3d4e5f6",
"status": "COMPLETED",
"input": {
"lat": 19.07,
"lon": 72.87,
"horizons": [
"baseline",
"2030",
"2040",
"2050"
],
"scenarios": [
"ssp126",
"ssp245",
"ssp585"
]
},
"location_context": {
"elevation_m": 3.0,
"distance_to_coast_km": 0.4,
"terrain_type": "flat",
"land_cover": "built_area",
"climate_zone": "subtropical"
},
"composite_score": {
"ssp126": {
"baseline": {"score": 1.63, "rating": "Moderate", "confidence": "High"},
"2030": {"score": 2.34, "rating": "Moderate", "confidence": "High"},
"2040": {"score": 2.34, "rating": "Moderate", "confidence": "High"},
"2050": {"score": 2.4, "rating": "Moderate", "confidence": "High"}
},
"ssp245": {
"baseline": {"score": 1.63, "rating": "Moderate", "confidence": "High"},
"2030": {"score": 2.28, "rating": "Moderate", "confidence": "High"},
"2040": {"score": 2.4, "rating": "Moderate", "confidence": "High"},
"2050": {"score": 2.5, "rating": "Moderate", "confidence": "High"}
},
"ssp585": {
"baseline": {"score": 1.63, "rating": "Moderate", "confidence": "High"},
"2030": {"score": 2.03, "rating": "Moderate", "confidence": "High"},
"2040": {"score": 2.24, "rating": "Moderate", "confidence": "High"},
"2050": {"score": 2.5, "rating": "Moderate", "confidence": "High"}
}
},
"hazards": {
"heat_wave": {
"metric": "additional_heat_wave_days",
"metric_unit": "days/year",
"baseline_value": 32.2,
"risk_matrix": {
"ssp126": {"baseline": "Low", "2030": "High", "2040": "High", "2050": "High"},
"ssp245": {"baseline": "Low", "2030": "High", "2040": "High", "2050": "Severe"},
"ssp585": {"baseline": "Low", "2030": "High", "2040": "High", "2050": "Severe"}
},
"values": {
"ssp126": {"baseline": 0.0, "2030": 17.8, "2040": 22.7, "2050": 28.2},
"ssp245": {"baseline": 0.0, "2030": 15.9, "2040": 24.4, "2050": 33.4},
"ssp585": {"baseline": 0.0, "2030": 18.3, "2040": 27.1, "2050": 36.6}
},
"data_available": {
"ssp126": {"baseline": true, "2030": true, "2040": true, "2050": true},
"ssp245": {"baseline": true, "2030": true, "2040": true, "2050": true},
"ssp585": {"baseline": true, "2030": true, "2040": true, "2050": true}
},
"status_matrix": {
"ssp126": {"baseline": "OK", "2030": "OK", "2040": "OK", "2050": "OK"},
"ssp245": {"baseline": "OK", "2030": "OK", "2040": "OK", "2050": "OK"},
"ssp585": {"baseline": "OK", "2030": "OK", "2040": "OK", "2050": "OK"}
},
"data_source": null,
"context": {
"method": "mean_plus_offset",
"threshold_celsius": 37.2,
"baseline_tasmax_mean": 32.2
},
"status": "OK",
"error": null
}
// ... 11 more hazards
},
"top_risks": [
{"hazard": "heat_wave", "scenario": "ssp245", "horizon": "2050", "rating": "Severe"},
{"hazard": "heat_wave", "scenario": "ssp585", "horizon": "2050", "rating": "Severe"},
{"hazard": "sea_level_rise", "scenario": "ssp126", "horizon": "2030", "rating": "Severe"}
],
"metadata": {
"baseline_period": "1980-2010",
"generated_at": "2026-06-11T18:37:02.489851+00:00",
"method_version": "climate_risk_v1.0",
"processing_time_sec": 481
},
"error": null,
"report_id": "6f1d2c3b-8a9e-4b7c-9d0e-1f2a3b4c5d6e",
"report_url": "/climate-risk/reports/6f1d2c3b-8a9e-4b7c-9d0e-1f2a3b4c5d6e/"
}
Errors
| Status | Reason |
|---|---|
409 |
Job still running: {"error": "Job not completed", "status": "RUNNING"} |
404 |
Job not found |
Partial results
If SSP1-2.6 projections are temporarily unavailable, the job still completes and the jobs endpoint reports COMPLETED. The results body then carries "status": "PARTIAL". SSP2-4.5 and SSP5-8.5 are complete. Under ssp126, the 2030, 2040 and 2050 cells are null in risk_matrix and values for the climate-driven hazards, with data_available false and status_matrix "DATA_UNAVAILABLE"; baseline cells, sea_level_rise and water_stress stay populated. The ssp126 composite for those horizons carries "confidence": "Low" ("score": null and "rating": "Unknown" if no hazard has data). In a batch, the item’s status reads COMPLETED and item.result.status reads PARTIAL. Check data_available or status_matrix before reading a rating.
{
"status": "PARTIAL",
"hazards": {
"heat_wave": {
"risk_matrix": {"ssp126": {"baseline": "Low", "2030": null, "2040": null, "2050": null}, ...},
"values": {"ssp126": {"baseline": 0.0, "2030": null, "2040": null, "2050": null}, ...},
"data_available": {"ssp126": {"baseline": true, "2030": false, "2040": false, "2050": false}, ...},
"status_matrix": {"ssp126": {"baseline": "OK", "2030": "DATA_UNAVAILABLE", "2040": "DATA_UNAVAILABLE", "2050": "DATA_UNAVAILABLE"}, ...},
"status": "OK"
},
"water_stress": {
"risk_matrix": {"ssp126": {"baseline": "Low", "2030": "Low", "2040": "Low", "2050": "Low"}, ...},
"status": "OK"
}
},
"composite_score": {
"ssp126": {
"baseline": {"score": 1.49, "rating": "Low", "confidence": "High"},
"2030": {"score": 1.94, "rating": "Moderate", "confidence": "Low"},
"2040": {"score": 1.94, "rating": "Moderate", "confidence": "Low"},
"2050": {"score": 1.94, "rating": "Moderate", "confidence": "Low"}
},
...
}
}
/api/v1/climate-risk/batch/analyze/Submit a batch of up to 5,000 locations for climate risk assessment. Returns 202 Accepted with a batch_id, then poll. See Batch & Async Pattern for the full flow.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
locations |
array | Yes | — | Array of {lat, lon, label?} objects (max 5,000) |
locations[].lat |
number | Yes | — | Latitude (-90 to 90) |
locations[].lon |
number | Yes | — | Longitude (-180 to 180) |
locations[].label |
string | No | — | User-provided label that round-trips back in the results |
dry_run |
boolean | No | false |
Return mock results without consuming quota. See Sandbox |
Response (202 Accepted)
{
"batch_id": "batch_climate_a1b2c3d4e5f6",
"status": "PROCESSING",
"total": 2,
"skipped": 0,
"tools_batch_id": "8c5e1a2b-3d4f-4a6b-9c7d-0e1f2a3b4c5d",
"batch_url": "/climate-risk/batch/8c5e1a2b-3d4f-4a6b-9c7d-0e1f2a3b4c5d/"
}
Rows with invalid coordinates are skipped and counted in skipped. A dry-run batch returns "status": "COMPLETED" on submit.
Errors
| Status | Reason |
|---|---|
400 |
Missing or empty locations array, exceeds 5,000 location limit, no valid coordinates in the array |
/api/v1/climate-risk/batch/{batch_id}/Get batch status and progress. Same shape as the cross-API batch status pattern.
Response (200 OK)
{
"batch_id": "batch_climate_a1b2c3d4e5f6",
"status": "PROCESSING",
"progress": {
"total": 3,
"completed": 2,
"failed": 0,
"percent_complete": 66
},
"tools_batch_id": "8c5e1a2b-3d4f-4a6b-9c7d-0e1f2a3b4c5d",
"batch_url": "/climate-risk/batch/8c5e1a2b-3d4f-4a6b-9c7d-0e1f2a3b4c5d/"
}
/api/v1/climate-risk/batch/{batch_id}/results/Get paginated batch results. Each result item carries the original label and the full per-location climate risk payload.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number |
page_size |
integer | 100 | Results per page (max 500) |
status |
string | — | Filter by job status: COMPLETED, FAILED |
Response Field Reference
The complete reference for every field returned by the climate risk results endpoint.
| Field path | Type | Meaning |
|---|---|---|
status |
string | COMPLETED or PARTIAL (see Partial results above) |
input |
object | Echo of the request coordinates with all three scenarios and four horizons |
composite_score.{scenario}.{horizon}.score |
number (1 to 5) or null | Aggregate risk score across all 12 hazards |
composite_score.{scenario}.{horizon}.rating |
string | Categorical rating: Low / Moderate / High / Severe / Extreme, or Unknown when score is null |
composite_score.{scenario}.{horizon}.confidence |
string | High, Medium, Low or None: the share of hazards with data for that cell |
hazards.{hazard}.metric |
string | Underlying metric name (e.g., additional_heat_wave_days) |
hazards.{hazard}.metric_unit |
string | Unit for the metric (e.g., days/year, meters, %) |
hazards.{hazard}.baseline_value |
number | Reference-period input to the hazard method; its unit can differ from metric_unit (heat_wave: baseline mean daily maximum, °C) |
hazards.{hazard}.risk_matrix.{scenario}.{horizon} |
string or null | Categorical rating for this hazard at this scenario/horizon |
hazards.{hazard}.values.{scenario}.{horizon} |
number or null | Numeric metric value at this scenario/horizon |
hazards.{hazard}.data_available.{scenario}.{horizon} |
boolean | false when the cell has no data |
hazards.{hazard}.status_matrix.{scenario}.{horizon} |
string | OK, DATA_UNAVAILABLE, NOT_APPLICABLE or CALCULATION_ERROR |
hazards.{hazard}.context |
object | Hazard-specific context (e.g., threshold_celsius for heat hazards) |
hazards.river_flood.context.flood_zone |
string | floodplain, near_floodplain, not_floodplain or not_modelled: where the site sits on the JRC river flood map, read over a 6 km box. At not_floodplain every cell is 0.0 and rated Low. At not_modelled every cell is null with status_matrix "DATA_UNAVAILABLE", the hazard status stays OK and the composite leaves the hazard out. Results before climate_risk_v1.2 carry the terrain classes floodplain, transition, slope or upland |
hazards.river_flood.context.flood_zone_factor |
number or null | Multiplies the rainfall measure: 1.0 at floodplain, 0.0 at not_floodplain, null at not_modelled. Near the floodplain it is 0.6, 0.3 or 0.1 as the nearest mapped cell is 1 to 250 m, 251 to 1,000 m or over 1,000 m away |
hazards.river_flood.context.floodplain |
object | The map reading: dataset, license, attribution, layer_status, depth at the site cell per return period and nearest_real_cell_m. The CC BY 4.0 credit travels with the class when you print it. Absent on results before climate_risk_v1.2 |
hazards.{hazard}.status |
string | Per-hazard status: OK or FAILED |
top_risks[] |
array | Up to 3 highest-rated hazard, scenario and horizon combinations, drawn from all scenarios and horizons including baseline |
location_context.elevation_m |
number | Elevation above sea level (meters) |
location_context.distance_to_coast_km |
number | Distance in km to the nearest sea coastline, measured on the OpenStreetMap coastline. Lakes, rivers and reservoirs do not count, and the Caspian Sea is excluded. Results with method_version climate_risk_v1.0 measured the distance to any permanent water |
location_context.coastline |
object | Coastline source: dataset, version and attribution (© OpenStreetMap contributors, ODbL 1.0). null on climate_risk_v1.0 results |
location_context.terrain_type |
string | flat, gentle, hilly, mountainous or very_steep |
location_context.land_cover |
string | Dominant land cover at the location (e.g., built_area, cropland, tree_cover) |
metadata.baseline_period |
string | Reference period for baseline values (typically 1980-2010) |
metadata.method_version |
string | Versioned methodology identifier: climate_risk_v1.2 since the river flood change on 10 November 2026, climate_risk_v1.1 from 9 November 2026, climate_risk_v1.0 before |
metadata.processing_time_sec |
number | Server-side processing time |
Code Examples
Submit a single-location climate risk assessment. The result covers all three scenarios at baseline, 2030, 2040 and 2050:
curl -X POST https://api.continuuiti.com/api/v1/climate-risk/analyze/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"lat": 28.6139,
"lon": 77.2090
}'
import requests
response = requests.post(
"https://api.continuuiti.com/api/v1/climate-risk/analyze/",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"lat": 28.6139, "lon": 77.2090},
)
job_id = response.json()["job_id"]
const response = await fetch(
"https://api.continuuiti.com/api/v1/climate-risk/analyze/",
{
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
lat: 28.6139,
lon: 77.2090,
}),
}
);
const { job_id } = await response.json();
Worked End-to-End Example
Real integrations stitch APIs together. The example below geocodes an address, submits a climate risk assessment, polls until completion, and prints the top three projected hazards under SSP5-8.5 at 2050. This is the consultant-ICP “first useful run.”
import requests
import time
API_BASE = "https://api.continuuiti.com/api/v1"
HEADERS = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}
# Step 1: Geocode an address to coordinates
address = "350 Fifth Avenue, New York, NY"
geo_response = requests.post(
f"{API_BASE}/geocode/",
headers=HEADERS,
json={"address": address},
)
geo = geo_response.json()
lat, lon = float(geo["latitude"]), float(geo["longitude"])
print(f"Geocoded {address} -> ({lat}, {lon})")
# Step 2: Submit a climate risk assessment (every result covers all three scenarios and four horizons)
submit_response = requests.post(
f"{API_BASE}/climate-risk/analyze/",
headers=HEADERS,
json={"lat": lat, "lon": lon},
)
job_id = submit_response.json()["job_id"]
print(f"Submitted climate risk job: {job_id}")
# Step 3: Poll until the job finishes
while True:
status_response = requests.get(f"{API_BASE}/climate-risk/jobs/{job_id}/", headers=HEADERS)
state = status_response.json()
print(f"Status: {state['status']}")
if state["status"] == "FAILED":
raise SystemExit(f"Job failed: {state['error']}")
if state["status"] == "COMPLETED":
break
time.sleep(10)
# Step 4: Fetch results and rank the hazards under SSP5-8.5 at 2050
results = requests.get(f"{API_BASE}/climate-risk/results/{job_id}/", headers=HEADERS).json()
ORDER = {"Low": 1, "Moderate": 2, "High": 3, "Severe": 4, "Extreme": 5}
ranked = []
for hazard, data in results["hazards"].items():
if data["status"] == "OK" and data["status_matrix"]["ssp585"]["2050"] == "OK":
ranked.append((ORDER[data["risk_matrix"]["ssp585"]["2050"]], hazard))
top_3 = [hazard for _, hazard in sorted(ranked, reverse=True)[:3]]
print(f"\nTop 3 climate hazards for {address} under SSP5-8.5 at 2050:")
for hazard in top_3:
data = results["hazards"][hazard]
rating = data["risk_matrix"]["ssp585"]["2050"]
value = data["values"]["ssp585"]["2050"]
baseline = data["values"]["ssp585"]["baseline"]
unit = data["metric_unit"]
print(f" {hazard}: {rating} ({value:.1f} {unit}, {value - baseline:+.1f} from baseline)")
Geocoder API
The Continuuiti Geocoder API converts text addresses to geographic coordinates with a structured quality envelope on every response. Pre-validation rejects clearly unusable input before consuming a provider call. Batch endpoints accept up to 5,000 addresses per submission and complete in a few minutes; a 10,000-address workload (two submissions) takes about ten minutes.
Quality Envelope
Every geocoding response includes a structured quality object so you can route results by confidence. The match_type tier is the most actionable field for downstream filtering.
| Match type | Approx accuracy | Description |
|---|---|---|
building |
~10 m | Matched to a specific building footprint |
address |
~20 m | Matched to a specific house number on a street |
street |
~100 m | Matched to a street; coordinates at street centroid |
city |
~1 km | Matched to a city or suburb centroid |
country |
~100 km | Matched to country only |
| Quality status | Meaning |
|---|---|
PASSED |
Address geocoded successfully; coordinates available |
REJECTED |
Address failed pre-validation; no provider call made (saves quota) |
FAILED |
Address passed pre-validation but no match found |
Garbage Detection (pre-validation)
Before any provider call, the API runs five quality checks to reject obviously unusable input. Rejected requests return 200 OK (not 400) because validation succeeded — the address was correctly identified as unusable.
| Check | Triggers when | Example |
|---|---|---|
too_short |
Address length < 5 characters | “NYC” |
no_alphanumeric |
Zero letters or numbers | “—///—“ |
suspicious_characters |
Contains emoji or characters like <>{} | “123 Main 🚀” |
repeated_chars |
Same character appears 5+ times | “aaaaaaa” |
no_spaces |
No whitespace in addresses >10 characters | “123MainStreetNewYork” |
Endpoints
/api/v1/geocode/Geocode a single address synchronously. Returns 201 Created with coordinates and quality, or 200 OK with a REJECTED quality envelope for garbage input.
Request body: {"address": "string (3–500 chars)"}
{
"id": 1,
"original_address": "350 Fifth Avenue, New York, NY",
"formatted_address": "350 5th Ave, New York, NY 10118, USA",
"latitude": "40.7484405",
"longitude": "-73.9856644",
"quality": {"score": 1.0, "status": "PASSED", "issues": [], "match_type": "building"},
"created_at": "2026-04-27T10:19:05Z"
}
Errors: 400 invalid request (missing address, <3 or >500 chars), 404 address not found by provider, 503 transient provider error after retries.
/api/v1/exports/geocoding/reports/{id}/Retrieve a specific previously geocoded address by ID through the export API. Returns 404 if not found.
/api/v1/geocode/batch/Submit a batch of up to 5,000 addresses for asynchronous geocoding. Same flow as the cross-cutting batch pattern.
| Field | Type | Required | Description |
|---|---|---|---|
addresses |
array | Yes | Array of address objects (max 5,000) |
addresses[].address |
string | Yes | Address string (3–500 chars) |
addresses[].label |
string | No | User-provided label, round-trips back in results |
dry_run |
boolean | No | Return mock coordinates without provider calls. See Sandbox |
/api/v1/geocode/batch/{batch_id}/Get batch status and progress. Batch status values: PENDING, PROCESSING, COMPLETED, FAILED. Per-address status uses PENDING, SUCCESS, REJECTED, FAILED; REJECTED (garbage input) addresses are counted separately and do not fail the batch.
/api/v1/geocode/batch/{batch_id}/results/Get paginated geocoding results. Query params: page (default 1), page_size (default 100, max 500), status (filter by job status).
Response Field Reference
| Field | Type | Meaning |
|---|---|---|
id |
integer | Server-assigned record ID |
original_address |
string | Address as submitted (verbatim) |
formatted_address |
string | Provider’s canonical formatting |
latitude |
string (decimal) | Geographic latitude |
longitude |
string (decimal) | Geographic longitude |
quality.score |
number (0–1) | Confidence score; 0 for REJECTED, null for FAILED |
quality.status |
string | PASSED, REJECTED, or FAILED |
quality.issues |
array | Pre-validation issue codes (see Garbage Detection) |
quality.match_type |
string | Match precision tier |
created_at |
string (ISO 8601) | Timestamp of geocoding |
Code Examples
curl -X POST https://api.continuuiti.com/api/v1/geocode/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"address": "350 Fifth Avenue, New York, NY"}'
import requests
response = requests.post(
"https://api.continuuiti.com/api/v1/geocode/",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"address": "350 Fifth Avenue, New York, NY"},
)
data = response.json()
print(data["latitude"], data["longitude"], data["quality"]["match_type"])
const response = await fetch("https://api.continuuiti.com/api/v1/geocode/", {
method: "POST",
headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ address: "350 Fifth Avenue, New York, NY" }),
});
const data = await response.json();
console.log(data.latitude, data.longitude, data.quality.match_type);
LULC API
The Continuuiti LULC API analyzes land use and land cover at any location and over any year range from 2016 to the current year. Returns annual satellite composites and land cover classification across 9 classes for areas of interest from 100 m to 10 km. Processing is asynchronous; typical 6-year analysis runs in 5 to 10 minutes.
9 Land Cover Classes
| Class key | Code | Label |
|---|---|---|
water |
0 | Water |
trees |
1 | Trees |
grass |
2 | Grass |
flooded_veg |
3 | Flooded Vegetation |
crops |
4 | Crops |
shrub |
5 | Shrub & Scrub |
built |
6 | Built Area |
bare |
7 | Bare Ground |
snow |
8 | Snow & Ice |
Quality Indicators
| Quality | Meaning |
|---|---|
OK |
≥60% cloud-free coverage; reliable results |
LOW_QUALITY |
<60% cloud-free coverage; use with caution |
FAILED |
Unable to process this year |
Endpoints
/api/v1/lulc/analyze/Submit a single LULC analysis job. Returns 202 Accepted with a job_id.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
lat |
number | Yes | — | Latitude (-90 to 90) |
lon |
number | Yes | — | Longitude (-180 to 180) |
size_m |
integer | No | 1000 | Area-of-interest size in meters (100–10000) |
start_year |
integer | No | 2020 | Start year (min 2016) |
end_year |
integer | No | 2025 | End year (max 2025) |
/api/v1/lulc/jobs/{job_id}/Get job status. Status values: PENDING, RUNNING, COMPLETED, FAILED. Progress object reports years_completed / years_total.
/api/v1/lulc/results/{job_id}/Get full results: per-year RGB composites, classified LULC raster URLs, per-year class percentages, and change-summary deltas across the year range.
{
"job_id": "lulc_a1b2c3d4e5f6",
"status": "COMPLETED",
"input": {"lat": 28.6139, "lon": 77.2090, "size_m": 1000, "start_year": 2020, "end_year": 2025},
"year_status": {"2020": "OK", "2021": "OK", "2022": "LOW_QUALITY", "2023": "OK", "2024": "OK", "2025": "OK"},
"artifacts": {
"rgb_frames": {"2020": {"url": "https://storage.example.com/.../2020.webp", "cloud_free_pct": 95.5}},
"lulc_frames": {"2020": {"url": "https://storage.example.com/.../2020.png"}}
},
"stats": {
"by_year": {
"2020": {"water_pct": 2.5, "trees_pct": 15.3, "built_pct": 45.2}
}
},
"change_summary": {
"from_year": 2020, "to_year": 2025,
"net_change_pp": {"built_pct": 4.3, "trees_pct": -2.1},
"top_increases": [{"class": "built", "label": "Built Area", "change_pp": 4.3}],
"top_decreases": [{"class": "trees", "label": "Trees", "change_pp": -2.1}]
}
}
/api/v1/lulc/batch/analyze/Submit a batch of up to 5,000 locations. Body fields mirror the single-location request, with an additional locations array (each with {lat, lon, label?}) and an optional dry_run flag.
/api/v1/lulc/batch/{batch_id}/Get batch status and progress. Same shape as the cross-API batch status pattern.
/api/v1/lulc/batch/{batch_id}/results/Get paginated batch results. Each result contains the per-location LULC payload.
Response Field Reference
| Field path | Type | Meaning |
|---|---|---|
year_status.{year} |
string | OK, LOW_QUALITY, or FAILED per year |
artifacts.rgb_frames.{year}.url |
string (URL) | Annual RGB satellite composite (WebP) |
artifacts.rgb_frames.{year}.cloud_free_pct |
number | Cloud-free coverage percentage |
artifacts.lulc_frames.{year}.url |
string (URL) | Annual classified LULC raster (PNG) |
stats.by_year.{year}.{class}_pct |
number | Per-class percentage of AOI for the year |
change_summary.net_change_pp.{class}_pct |
number | Percentage-point delta from start to end year |
change_summary.top_increases[] |
array | Classes with biggest gains |
change_summary.top_decreases[] |
array | Classes with biggest losses |
Code Examples
curl -X POST https://api.continuuiti.com/api/v1/lulc/analyze/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"lat": 28.6139, "lon": 77.2090, "size_m": 1000, "start_year": 2020, "end_year": 2025}'
import requests
response = requests.post(
"https://api.continuuiti.com/api/v1/lulc/analyze/",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"lat": 28.6139, "lon": 77.2090, "size_m": 1000,
"start_year": 2020, "end_year": 2025},
)
job_id = response.json()["job_id"]
const response = await fetch("https://api.continuuiti.com/api/v1/lulc/analyze/", {
method: "POST",
headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
lat: 28.6139, lon: 77.2090, size_m: 1000,
start_year: 2020, end_year: 2025,
}),
});
const { job_id } = await response.json();
Biodiversity API
Biodiversity screening (Hansen deforestation, OpenStreetMap protected areas (© OpenStreetMap contributors, ODbL 1.0), RESOLVE ecoregions, EUDR indicators) is bundled into every LULC+ analysis. There is no separate endpoint to call. Submit via POST /api/v1/lulc/analyze/ (or the batch variant) and the biodiversity results are included in the report, retrievable via GET /api/v1/exports/lulc/reports/{id}/ (the biodiversity field) once the analysis completes.
CDEM API
The CDEM API resolves one coordinate into a site bundle: a bare-earth elevation grid, building footprints and road centrelines over a 2 km square centred on the point, aligned on one grid with the terrain’s per-cell vertical uncertainty carried alongside. It is the terrain that the Pluvial & Flash Flood API solves on, and a reusable input for any site model of your own. A bundle is about 1 MB and is served from storage; the API returns the summary and a bundle_url.
/api/v1/cdem/analyze/Submit a site. 202 with a job_id, or 200 inline for dry_run.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
lat / lon |
number | Yes | n/a | WGS84 coordinate |
half_km |
number | No | 1.0 | Half-width of the box in km (1.0 is the 2 km box the pluvial service takes) |
label |
string | No | “” | Your own label |
dry_run |
boolean | No | false | Sandbox: deterministic mock bundle inline, free |
/api/v1/cdem/jobs/{job_id}/Job status with per-layer progress.
/api/v1/cdem/results/{job_id}/Summary with bundle_url and per-layer status.
/api/v1/cdem/batch/analyze/Submit a batch of sites.
/api/v1/cdem/batch/{batch_id}/Batch status (standard pattern).
/api/v1/cdem/batch/{batch_id}/results/Paginated batch results.
A terrain job takes 30 s to 3 minutes and costs 1 credit. Each layer carries its own status (ok, empty_confirmed, unavailable, failed), license and attribution; a PARTIAL job still carries a usable bundle when the terrain layer is fine. Read the layer status before consuming a layer: an empty building layer that is empty_confirmed is open land, while failed means the fetch did not succeed and the site should be fetched again.
Code Examples
curl -X POST https://api.continuuiti.com/api/v1/cdem/analyze/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"lat": 28.6139, "lon": 77.2090, "half_km": 1.0, "label": "site-01"}'
import requests, time
API = "https://api.continuuiti.com/api/v1"
H = {"X-API-Key": "YOUR_API_KEY"}
job = requests.post(f"{API}/cdem/analyze/", headers=H,
json={"lat": 28.6139, "lon": 77.2090, "half_km": 1.0, "label": "site-01"}).json()
while requests.get(f"{API}/cdem/jobs/{job['job_id']}/", headers=H).json()["status"] not in ("COMPLETED", "PARTIAL", "FAILED"):
time.sleep(8)
summary = requests.get(f"{API}/cdem/results/{job['job_id']}/", headers=H).json()
print({name: layer["status"] for name, layer in summary["layers"].items()})
bundle = requests.get(summary["bundle_url"]).json() # no key needed
const API = "https://api.continuuiti.com/api/v1";
const headers = { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" };
const { job_id } = await (await fetch(`${API}/cdem/analyze/`, {
method: "POST", headers,
body: JSON.stringify({ lat: 28.6139, lon: 77.2090, half_km: 1.0, label: "site-01" }),
})).json();
Pluvial & Flash Flood API
The Continuuiti Pluvial & Flash Flood API simulates rain falling on a site and moving over the ground. You give it a terrain you already hold (a 2 km site bundle from the CDEM API) and a list of one to sixteen storms, each a rainfall depth over a duration. For every storm it returns the depth of water in every cell at the end of the run, the deepest water each cell saw at any moment, the first moment each cell reached 0.3 m and 0.5 m, a depth curve every 30 seconds at the site and at any probe you name, and statistics over the box. The same solver that runs the pluvial flood simulator on the platform runs here, so an API result and a simulator run on the same terrain and storm agree.
Two readings come from one request:
| Reading | Storm | The figures that carry it |
|---|---|---|
| Pluvial flooding | 60 minutes at the stated depth | depth at the two-hour mark, the peak beside it, the water grid around the site |
| Flash flooding | under 60 minutes at the stated depth | the depth curve, the peak and the minute it was reached, the minutes to 0.3 m and 0.5 m, the arrival maps |
A one-hour storm is the standard pluvial reading. A ten, fifteen or thirty minute burst is the flash flood reading, where the end state understates what happened and the time dimension carries the signal: how fast the water arrived, how deep it got, and when. Both readings use the same physics, the same terrain and the same two-hour window, so a request can carry both storms side by side and the figures compare directly.
The rainfall is yours. Each storm is a depth in millimetres over a duration in minutes, plus a free-text source naming the rainfall curve it came from. The service takes the depth and the duration and echoes the source; the choice of return period and curve stays with you.
How it works
- Terrain. The 2 km bare-earth grid at 30 m is subdivided to a routing mesh of about 10 m. Every building footprint is stamped into the mesh as a solid block so water flows around it, and rain that lands on a roof is delivered to the ground beside it, the way a downspout does.
- Physics. A local-inertial shallow water solver after Bates, Horritt and Fewtrell (2010) with the de Almeida et al. (2012) flux and building blocks after Schubert and Sanders (2012). Mass is conserved to machine precision on every run and the result carries its own mass balance figure.
- Open boundary. Water that reaches the edge of the box leaves it; the response reports how much.
- Thresholds. 0.3 m and 0.5 m are the still-water depth limits above which flood water becomes unsafe for small vehicles, and then for vehicles, children and the elderly, in Australian Disaster Resilience Guideline 7-3 (2017). Every cell records the first moment it crossed each one.
Endpoints
/api/v1/pluvial/analyze/Submit one site with its storms. 202 with a job_id, or 200 inline for dry_run.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
cdem_job_id |
string | Yes | n/a | A completed CDEM terrain job of yours at the standard 2 km box |
label |
string | No | “” | Your own label, up to 120 characters |
storms |
array | Yes | n/a | 1 to 16 storms, each {id, rain_mm, duration_min, source} |
storms[].id |
string | Yes | n/a | Unique within the job, letters, digits, _ and -, up to 40 characters |
storms[].rain_mm |
number | Yes | n/a | Rainfall depth, above 0 and up to 1000 |
storms[].duration_min |
number | Yes | n/a | Storm duration in minutes, from 1 up to stop_min |
storms[].source |
string | No | “” | The rainfall curve the depth came from, echoed back, up to 200 characters |
stop_min |
integer | No | 120 | Minutes simulated from the start of rain, 30 to 180 |
probes |
array | No | [] | Up to 18 {lat, lon, label} inside the box; the site is always probed |
snapshots_min |
array | No | [] | Up to 6 minutes at which the whole depth grid is captured |
dry_run |
boolean | No | false | Sandbox: deterministic mock, inline, free (up to 4 storms and 2 snapshots) |
Submit answers 202 {"job_id": "plv_...", "status": "PENDING", "storm_count": 2, "credits_remaining": 980, "report_url": "/api/v1/exports/pluvial/reports/<id>/"}. Credits: one per storm, deducted at submit.
/api/v1/pluvial/jobs/{job_id}/Job status with per-storm progress.
Status values: PENDING, QUEUED, PROCESSING, COMPLETED, PARTIAL, FAILED. While processing, progress carries one line per storm ("RP50-60min": "t=48.0 min, steps 3000", then "ok"); each storm’s final status is in the results summary. A two-storm job completes in one to two minutes; a sixteen-storm flash set in about three to six.
/api/v1/pluvial/results/{job_id}/The summary with a URL per storm object and the terrain object.
One block per storm, the terrain and mesh description, the solver settings, the methodology notes, and a URL for each storm’s grids and for the terrain. This is a real response, trimmed to one storm:
{
"job_id": "plv_5130d4702bed",
"status": "COMPLETED",
"method_version": "pluvial_v1.0",
"input": {"cdem_job_id": "cdem_15bdbc0fa2dc", "label": "site-01", "stop_min": 120,
"probes": [{"lat": 28.615, "lon": 77.21, "label": "loading bay"}], "snapshots_min": [30, 60]},
"terrain": {"cdem_job_id": "cdem_15bdbc0fa2dc", "cdem_status": "COMPLETED", "half_km": 1.0,
"layers": {"dem": {"status": "ok", "dataset": "...", "license": "...", "attribution": "...", "vertical_error_m": 1.15},
"buildings": {"status": "ok", "count": 471, "source": "...", "license": "...", "attribution": "..."}}},
"mesh": {"width": 222, "height": 195, "dx_m": 9.05, "dy_m": 10.31,
"origin": {"lat": 28.6229, "lon": 77.1988}, "row_order": "north_to_south",
"subdivision_k": 3, "source_grid": {"width": 74, "height": 65}},
"solver": {"engine": "... (local-inertial shallow water, Bates et al. 2010; de Almeida et al. 2012 flux)", "k": 3, "default_wall_m": 5, "n": 0.05,
"boundary": "open", "roof_rain": "redirected to the nearest ground cell"},
"storms": [
{"id": "RP50-15min", "status": "ok", "reason": null, "solver_code": null,
"rain_mm": 38, "duration_min": 15, "stop_min": 120,
"source": "illustrative design storm; replace with your own rainfall curve",
"solve": {"steps": 5927, "t_seconds": 7200.82, "mass_error": 5.5e-15},
"site": {"row": 97, "col": 110, "ground_m": 213.17, "on_building": false,
"depth_end_m": 0.0321, "t_end_s": 7200.82, "peak_m": 0.0321, "peak_t_s": 7200.82,
"arrival_03_s": null, "arrival_05_s": null},
"probes": [{"label": "loading bay", "row": 85, "col": 121, "ground_m": 212.8, "on_building": false,
"depth_end_m": 0.0002, "peak_m": 0.0213, "peak_t_s": 900,
"arrival_03_s": null, "arrival_05_s": null}],
"box_all_cells": {"cells": 43290, "wet_cells": 5705, "max_depth_m": 3.177,
"stored_volume_m3": 139626.5, "boundary_outflow_m3": 13813.3},
"box_land_cells": {"cells": 40473, "wet_cells": 5702, "max_depth_m": 3.177, "p50_wet_depth_m": 0.177,
"ground_le_0p5m_cells": 0, "cells_reaching_0p3m": 2067, "cells_reaching_0p5m": 976},
"series_rows": 241, "global_series_rows": 241,
"bundle_url": "https://.../pluvial-bundles/plv_5130d4702bed/RP50-15min.json"},
{"id": "RP50-60min", "status": "ok", "...": "..."}
],
"limitations": ["...", "..."],
"bundle_urls": {"RP50-15min": "https://.../RP50-15min.json", "RP50-60min": "https://.../RP50-60min.json"},
"terrain_url": "https://.../pluvial-bundles/plv_5130d4702bed/terrain.json",
"expires_at": "2027-05-02T03:40:26+00:00"
}
Reading the numbers. depth_end_m is the water at the site cell at the end of the run; peak_m at peak_t_s is the deepest it got and when. In the example the 15 minute storm peaks at the loading bay 15 minutes in, at 2.1 cm, and has drained to a film by the two-hour mark. The site cell is still gathering water at the end of the run (its peak is at t_end_s), which is the signature of a hollow that fills. arrival_03_s and arrival_05_s are null when the cell never reached that depth.
When site.on_building is true the pin sits on a roof, which sheds its rain and reads dry. The block then carries a nearest_ground object with the same figures at the nearest ground cell and its distance in metres: quote that. box_land_cells counts ground above 0.5 m that is not a building. In a box that touches the sea it is the reading to quote, because bare-earth terrain carries no sea floor and sea cells pond rain; ground_le_0p5m_cells tells you how much of the box that is. The limitations list carries the methodology notes your own outputs should carry forward.
The stored objects. Each storm’s object holds the full grids: depth_mm and depth_max_mm (integers, row-major, row 0 north, index row x width + col), arrival_03_s and arrival_05_s (seconds, -1 never), the probe series ([t_s, h_m, win_max_m] every 30 seconds), a global series (wet cells, maximum depth, volumes, mass balance) and the snapshots you asked for. The terrain object holds the stamped surface, the bare ground and the building mask once for the job. Objects are fetched without a key from the URLs in the summary and kept for 180 days.
Per-storm status
| Status | Meaning |
|---|---|
ok |
solved to the stop time |
partial |
the solver stopped itself before the stop time; the figures are valid to t_end_s and reason says why |
failed |
no data for this storm; solver_code names the cause |
A job is COMPLETED when every storm is ok, PARTIAL when at least one storm carries data and not all are ok, and FAILED only when every storm failed. PARTIAL is terminal and carries its data.
/api/v1/pluvial/batch/analyze/Submit a batch of sites, each with its storms.
{"items": [{"cdem_job_id": "cdem_...", "label": "site-01", "storms": [...], "stop_min": 120},
{"cdem_job_id": "cdem_...", "label": "site-02", "storms": [...]}],
"dry_run": false}
Each item is the single-request body (sandbox: up to 4 storms each). Batch status and paginated results follow the standard pattern; each result item carries index, label, status, job_id, storm_statuses and result (the summary above).
/api/v1/pluvial/batch/{batch_id}/Batch status.
/api/v1/pluvial/batch/{batch_id}/results/Paginated per-site summaries.
/api/v1/exports/pluvial/reports/Your pluvial reports as JSON; /batches/{id}/csv/ for a batch as CSV.
Response Field Reference
| Field path | Type | Meaning |
|---|---|---|
storms[].status |
string | ok, partial or failed |
storms[].solve.steps |
integer | adaptive time steps taken |
storms[].solve.t_seconds |
number | simulated time reached (the stop time when ok) |
storms[].solve.mass_error |
number | relative mass balance error of the run |
storms[].site.on_building |
boolean | true when the site cell is a roof; read site.nearest_ground |
storms[].site.depth_end_m |
number | water depth at the site cell at t_end_s |
storms[].site.peak_m / peak_t_s |
number | deepest water at the site cell and when it occurred (seconds) |
storms[].site.arrival_03_s / arrival_05_s |
number or null | first moment the site cell reached 0.3 m / 0.5 m |
storms[].site.nearest_ground |
object | present when on_building: the same figures at the nearest ground cell, with distance_m |
storms[].probes[] |
array | the same block for each probe you named |
storms[].box_land_cells.wet_cells |
integer | land cells holding at least 1 mm at the end of the run |
storms[].box_land_cells.max_depth_m |
number | deepest water on land in the box |
storms[].box_land_cells.p50_wet_depth_m |
number | median depth over wet land cells |
storms[].box_land_cells.cells_reaching_0p3m / 0p5m |
integer | land cells that crossed each threshold at any moment |
storms[].box_all_cells.boundary_outflow_m3 |
number | water that left across the open box edges |
storms[].bundle_url |
string | the storm’s grid object |
terrain.layers.dem.vertical_error_m |
number | the terrain’s vertical uncertainty the run carries |
mesh |
object | mesh geometry: size, cell size, north-west origin, row order, subdivision |
limitations |
array | the methodology notes, in order, to carry forward with any figure |
terrain_url / expires_at |
string | the terrain object; when the objects are deleted (180 days) |
Code Examples
curl -X POST https://api.continuuiti.com/api/v1/pluvial/analyze/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"cdem_job_id": "cdem_15bdbc0fa2dc", "label": "site-01",
"storms": [
{"id": "RP50-60min", "rain_mm": 70, "duration_min": 60, "source": "your rainfall curve, 1 in 50"},
{"id": "RP50-15min", "rain_mm": 38, "duration_min": 15, "source": "your rainfall curve, 1 in 50"}
],
"snapshots_min": [30, 60]
}'
import requests, time
API = "https://api.continuuiti.com/api/v1"
H = {"X-API-Key": "YOUR_API_KEY"}
job = requests.post(f"{API}/pluvial/analyze/", headers=H, json={
"cdem_job_id": "cdem_15bdbc0fa2dc", "label": "site-01",
"storms": [
{"id": "RP50-60min", "rain_mm": 70, "duration_min": 60, "source": "your rainfall curve, 1 in 50"},
{"id": "RP50-15min", "rain_mm": 38, "duration_min": 15, "source": "your rainfall curve, 1 in 50"},
],
}).json()
while True:
status = requests.get(f"{API}/pluvial/jobs/{job['job_id']}/", headers=H).json()
if status["status"] in ("COMPLETED", "PARTIAL", "FAILED"):
break
time.sleep(8)
summary = requests.get(f"{API}/pluvial/results/{job['job_id']}/", headers=H).json()
for storm in summary["storms"]:
site = storm["site"].get("nearest_ground") or storm["site"]
print(storm["id"], storm["status"], f"peak {site['peak_m']:.2f} m at {site['peak_t_s']/60:.0f} min",
f"end {site['depth_end_m']:.2f} m")
grids = requests.get(summary["bundle_urls"]["RP50-15min"]).json() # no key needed
depth_mm = grids["depth_mm"] # width x height, row 0 north
const API = "https://api.continuuiti.com/api/v1";
const headers = { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" };
const { job_id } = await (await fetch(`${API}/pluvial/analyze/`, {
method: "POST", headers,
body: JSON.stringify({
cdem_job_id: "cdem_15bdbc0fa2dc", label: "site-01",
storms: [
{ id: "RP50-60min", rain_mm: 70, duration_min: 60, source: "your rainfall curve, 1 in 50" },
{ id: "RP50-15min", rain_mm: 38, duration_min: 15, source: "your rainfall curve, 1 in 50" },
],
}),
})).json();
let status;
do {
await new Promise(r => setTimeout(r, 8000));
status = await (await fetch(`${API}/pluvial/jobs/${job_id}/`, { headers })).json();
} while (!["COMPLETED", "PARTIAL", "FAILED"].includes(status.status));
const summary = await (await fetch(`${API}/pluvial/results/${job_id}/`, { headers })).json();
Worked End-to-End Example
One site, from a coordinate to a flash flood reading, in three calls.
import requests, time
API = "https://api.continuuiti.com/api/v1"
H = {"X-API-Key": "YOUR_API_KEY"}
def wait(service, job_id):
while True:
s = requests.get(f"{API}/{service}/jobs/{job_id}/", headers=H).json()
if s["status"] in ("COMPLETED", "PARTIAL", "FAILED"):
return s
time.sleep(8)
# 1. Terrain: a 2 km site bundle around the coordinate (about a minute; 1 credit)
terrain = requests.post(f"{API}/cdem/analyze/", headers=H,
json={"lat": 28.6139, "lon": 77.2090, "half_km": 1.0, "label": "site-01"}).json()
wait("cdem", terrain["job_id"])
# 2. Storms: the pluvial reading (60 min) and a flash flood reading (15 min) in one job (1 credit per storm)
job = requests.post(f"{API}/pluvial/analyze/", headers=H, json={
"cdem_job_id": terrain["job_id"], "label": "site-01",
"storms": [
{"id": "RP50-60min", "rain_mm": 70, "duration_min": 60, "source": "your rainfall curve, 1 in 50"},
{"id": "RP50-15min", "rain_mm": 38, "duration_min": 15, "source": "your rainfall curve, 1 in 50"},
],
"probes": [{"lat": 28.6150, "lon": 77.2100, "label": "loading bay"}],
}).json()
wait("pluvial", job["job_id"])
# 3. Read: the site block per storm, the probe curve, and the arrival maps from the storm object
summary = requests.get(f"{API}/pluvial/results/{job['job_id']}/", headers=H).json()
flash = next(s for s in summary["storms"] if s["id"] == "RP50-15min")
print("flash flood at the loading bay: peak %.3f m at minute %.0f" % (
flash["probes"][0]["peak_m"], flash["probes"][0]["peak_t_s"] / 60))
print("land cells that reached 0.3 m:", flash["box_land_cells"]["cells_reaching_0p3m"])
grids = requests.get(flash["bundle_url"]).json()
curve = grids["probes"][1]["series"] # [t_s, depth_m, window_max_m] every 30 s at the loading bay
arrival = grids["arrival_03_s"] # seconds to 0.3 m per cell, -1 where it never did
On the test account this ran in 55 s for the terrain and 2 min 14 s for the two storms; the printed lines were flash flood at the loading bay: peak 0.021 m at minute 15 and land cells that reached 0.3 m: 2067.
Sandbox
Add "dry_run": true to a submit. The response is inline (200), deterministic for the same input, unbilled, and carries the first storm’s grids and the terrain under bundle so your parser can be tested; the other storms get placeholder URLs. Dry runs accept up to 4 storms and 2 snapshots. A sandbox terrain job from the CDEM API qualifies as the terrain for a sandbox pluvial job, so a whole integration can be tested without spending a credit. Markers: method_version ends in _dry_run, float_family is dry_run, and the engine string is prefixed [DRY RUN].
Errors
All refusals answer 400 with {"error": "...", "code": "..."}.
| Code | When |
|---|---|
INVALID_INPUT |
a field outside its rule; the message states the rule (storm count, duplicate id, rain depth, duration above stop_min, probe or snapshot count) |
CDEM_JOB_NOT_YOURS |
the terrain job is not one of your CDEM jobs |
CDEM_JOB_UNAVAILABLE |
the terrain job is not complete, has expired, is a sandbox job on a real run, or was made before 2026-10-17 (fetch the terrain again) |
BOX_NOT_STANDARD |
the terrain job is not the 2 km box |
CDEM_BUILDINGS_FAILED |
the terrain’s building layer failed to fetch; fetch the terrain again |
PROBE_OUTSIDE_BOX |
a probe lies outside the terrain box; the message gives the box bounds |
Flood Field API
The Continuuiti Flood Field API returns the two flood hazard grids around a coordinate: the riverine (river) flood depth field at about 93 m and the coastal inundation field at about 930 m, each at four return periods, over a 6 km square centred on the site. You get the depth at the site cell, the nearest modelled cell and its distance, a per-band audit of every grid, and the grids themselves as flat arrays you can map. For flood under climate scenarios and future horizons, see the Climate Risk API.
| Return period | Annual chance | Common framing |
|---|---|---|
| RP10 | 10% per year | “10-year flood” |
| RP50 | 2% per year | “50-year flood” |
| RP100 | 1% per year | “100-year flood” |
| RP500 | 0.2% per year | “500-year flood” |
Riverine depths are metres above local ground. Coastal depths are metres of inundation at the 2010 sea-level reference, carried twice: without and with land subsidence (site_cell and site_cell_wtsub), the second being the one to read for delta and coastal cities.
Endpoints
/api/v1/flood-field/analyze/Submit a site. 202 with a job_id, or 200 inline for dry_run.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
lat / lon |
number | Yes | n/a | WGS84 coordinate |
label |
string | No | “” | Your own label |
half_km |
number | No | 3.0 | Half-width of the box in km, up to 3.0 (a 6 km box) |
dry_run |
boolean | No | false | Sandbox: deterministic mock with the grids inline, free |
Submit answers 202 {"job_id": "ff_...", "status": "PENDING"}. A site takes 30 to 60 seconds and costs 1 credit.
/api/v1/flood-field/jobs/{job_id}/Job status with per-layer progress.
/api/v1/flood-field/results/{job_id}/Summary with bundle_url.
A real response, trimmed to one audit band per layer:
{
"job_id": "ff_3f866e487ba7", "status": "COMPLETED", "method_version": "flood_field_v1.0",
"input": {"lat": 38.627, "lon": -90.1994, "half_km": 3.0, "label": "site-02"},
"bundle_url": "https://.../flood-field-bundles/ff_3f866e487ba7/bundle.json",
"expires_at": "2027-05-02T03:46:41+00:00",
"layers": {
"riverine": {
"status": "ok", "reason": null, "license": "...", "attribution": "...",
"return_periods": [10, 50, 100, 500], "depth_semantics": "depth above local ground, metres",
"width": 84, "height": 66, "dx_m": 72.47, "dy_m": 92.77, "dx_deg": 0.000833333, "dy_deg": 0.000833333,
"origin": {"lat": 38.65415, "lon": -90.23417, "is": "north_west_cell_centre"}, "row_order": "north_to_south",
"pin": {"row": 33, "col": 42, "idx": 2814},
"site_cell": {"RP10": null, "RP50": null, "RP100": null, "RP500": null},
"nearest_real_cell_m": 996,
"band_audit": {
"RP100_depth": {"valid_cells": 1832, "masked_cells": 3712, "real_cells": 1832,
"min": 0.135, "max": 13.737, "p50": 6.03}
}
},
"coastal": {
"status": "ok", "reason": null, "license": "...", "attribution": "...",
"reference": "historical, 2010 sea level, extreme sea level fitted to 1979 to 2014",
"width": 10, "height": 7, "dx_m": 724.7, "dy_m": 927.7,
"pin": {"row": 3, "col": 5, "idx": 35},
"site_cell": {"RP10": 0.0, "RP50": 0.0, "RP100": 0.0, "RP500": 0.0},
"site_cell_wtsub": {"RP10": 0.0, "RP50": 0.0, "RP100": 0.0, "RP500": 0.0}
}
}
}
Reading the numbers. A riverine site_cell value of null means the cell is outside the modelled floodplain of the river; nearest_real_cell_m tells you how far the nearest modelled cell is (996 m here, the river two blocks east). Riverine grids also carry a permanent_water_class band that marks the river channel itself, so you can tell when a pin lands in the river channel.
For the coastal layer, sea and unmodelled cells are null and dry land is 0.0. The band_audit block gives, for every band, how many cells hold a value, how many are masked, and the minimum, maximum and median over real cells, so you can judge a grid before mapping it. status per layer: ok, empty_confirmed (the source holds nothing in this box, which for an inland site’s coastal layer is the expected answer), unavailable (no coverage) or failed (not read; fetch again). A job is COMPLETED when both layers are ok or empty_confirmed, PARTIAL when one layer carries data and the other does not.
The bundle. bundle_url serves {"summary": ..., "bands": {"riverine": {"RP10_depth": [...], ..., "permanent_water_class": [...]}, "coastal": {...}}}, each band a flat array of width x height values, row 0 north, index row x width + col, cell centre at (origin.lat - row x dy_deg, origin.lon + col x dx_deg). About 270 KB, fetched without a key, kept for 180 days.
/api/v1/flood-field/batch/analyze/Submit a batch of sites as a locations array of lat, lon and label, with an optional top-level half_km for every site; each site gets all four return periods.
/api/v1/flood-field/batch/{batch_id}/Batch status (standard pattern).
/api/v1/flood-field/batch/{batch_id}/results/Paginated batch results.
Response Field Reference
| Field path | Type | Meaning |
|---|---|---|
layers.riverine.site_cell.{RP} |
number or null | riverine depth at the site cell, metres above ground; null outside the modelled floodplain |
layers.riverine.nearest_real_cell_m |
integer | distance to the nearest cell with a modelled depth |
layers.riverine.pin |
object | the site cell’s row, column and flat index |
layers.riverine.band_audit.{band} |
object | per-band counts and statistics: valid_cells, masked_cells, real_cells, min, max, p50 |
layers.coastal.site_cell.{RP} |
number or null | coastal inundation at the site cell without subsidence; null at sea |
layers.coastal.site_cell_wtsub.{RP} |
number or null | the same with land subsidence |
layers.{layer}.status |
string | ok, empty_confirmed, unavailable, failed |
layers.{layer}.origin, dx_deg, dy_deg, row_order |
mixed | georeferencing of the grid |
bundle_url / expires_at |
string | the grids; when they are deleted (180 days) |
Code Examples
curl -X POST https://api.continuuiti.com/api/v1/flood-field/analyze/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"lat": 38.627, "lon": -90.1994, "label": "site-02"}'
import requests, time
API = "https://api.continuuiti.com/api/v1"
H = {"X-API-Key": "YOUR_API_KEY"}
job = requests.post(f"{API}/flood-field/analyze/", headers=H,
json={"lat": 38.627, "lon": -90.1994, "label": "site-02"}).json()
while requests.get(f"{API}/flood-field/jobs/{job['job_id']}/", headers=H).json()["status"] not in ("COMPLETED", "PARTIAL", "FAILED"):
time.sleep(8)
summary = requests.get(f"{API}/flood-field/results/{job['job_id']}/", headers=H).json()
riv = summary["layers"]["riverine"]
print("RP100 at the site:", riv["site_cell"]["RP100"], "| nearest modelled cell:", riv["nearest_real_cell_m"], "m")
bundle = requests.get(summary["bundle_url"]).json() # no key needed
rp100 = bundle["bands"]["riverine"]["RP100_depth"] # width x height values, row 0 north
const API = "https://api.continuuiti.com/api/v1";
const headers = { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" };
const { job_id } = await (await fetch(`${API}/flood-field/analyze/`, {
method: "POST", headers, body: JSON.stringify({ lat: 38.627, lon: -90.1994, label: "site-02" }),
})).json();
Sandbox
"dry_run": true returns 200 with the summary and the full mock bundle inline, deterministic for the same input and on the real grid geometry, unbilled. method_version ends in _dry_run and the dataset and attribution strings are prefixed [DRY RUN].
Water Stress API
The Continuuiti Water Stress API returns the published water risk record for the basin a coordinate sits in. The record holds thirteen baseline indicators, ten industry risk profiles, twelve monthly values, and nine projected values across three pathways and three horizons (2030, 2050, 2080), each with its score, category and label as published. It also names the basin unit, shows how the coordinate was matched to it, and carries an audit that names every value that is a code, a floor or a placeholder. Synchronous: one GET, one record, no job to poll.
| Band | Baseline water stress | Meaning |
|---|---|---|
| Low | under 10% | withdrawals are a small share of available supply |
| Low to Medium | 10 to 20% | |
| Medium to High | 20 to 40% | |
| High | 40 to 80% | |
| Extremely High | over 80% | withdrawals approach or exceed available supply |
Endpoints
/api/v1/water-stress/query/?lat=&lon=&label=One site, synchronous.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
lat / lon |
number | Yes | n/a | WGS84 coordinate |
label |
string | No | “” | Your own label, echoed |
dry_run |
boolean | No | false | Sandbox: deterministic mock, free |
A real response, trimmed:
{
"status": "COMPLETED", "method_version": "water_stress_v1.0", "error": null,
"input": {"lat": 28.6139, "lon": 77.209, "label": "site-01"},
"dataset": {"name": "...", "version": "...", "license": "...", "attribution": "...",
"baseline_period": "1979-2019",
"future_windows": {"2030": "2015-2045", "2050": "2035-2065", "2080": "2065-2095"}},
"unit": {"string_id": "...", "country": "...", "province": "...", "area_km2": 412.0,
"match": {"method": "contains", "gap_m": 0}},
"baseline_annual": {
"status": "ok",
"indicators": {
"bws": {"name": "Baseline water stress", "raw": 0.977, "score": 4.29, "cat": 4, "label": "Extremely High (>80%)", "flag": null},
"bwd": {"name": "Baseline water depletion", "label": "Medium - High (25-50%)", "...": "..."},
"drr": {"name": "Drought risk", "label": "High (0.8-1.0)", "...": "..."},
"rfr": {"name": "Riverine flood risk", "label": "High (6 in 1,000 to 1 in 100)", "...": "..."},
"...": "13 indicators"
},
"profiles": {
"def": {"name": "Default", "qan": {"label": "Extremely High (4-5)", "...": "..."}, "tot": {"label": "Extremely High (4-5)", "...": "..."}},
"...": "10 profiles"
}
},
"baseline_monthly": {"status": "ok", "months": [
{"month": 1, "bws": {"raw": 0.147, "score": 1.56, "cat": 1, "label": "Low - Medium (10-20%)", "flag": null}, "...": "..."},
{"month": 4, "bws": {"label": "Extremely High (>80%)", "...": "..."}},
"... 12 entries"
]},
"future_annual": {"status": "ok", "pathways": {
"opt": {"name": "Optimistic", "horizons": {"2030": {"ws": {"raw": 0.78, "label": "High (40-80%)"}}, "...": "..."}},
"bau": {"name": "Business as usual", "horizons": {"2030": {"ws": {"raw": 0.53, "label": "High (40-80%)"}},
"2050": {"ws": {"raw": 0.93, "label": "Extremely high (>80%)"}}, "...": "..."}},
"pes": {"name": "Pessimistic", "...": "..."}
}},
"audit": {"bws_flag": null, "arid": false, "no_data": false,
"score_formula": "score = clamp(log2(raw / 0.1) + 1, 0, 5)",
"monthly_flagged_months": [], "future_cells": 9, "future_flagged_cells": 0}
}
Reading the numbers. raw is the ratio as published (0.977 means withdrawals at 98% of available supply), score is the 0 to 5 score, cat the band index and label the band name, all as published; the service adds only flag. The monthly table shows the seasonal shape (here the pre-monsoon months are Extremely High and the monsoon months Low). The projected values are the dataset’s own three pathways at 2030, 2050 and 2080, carried as published. unit.match.method says how the coordinate was placed: contains when a basin unit contains it, nearest with gap_m when the point sits on open water or a placeholder strip and the nearest unit within 2 km is used.
The flag field is the one thing to check before printing a value:
flag |
Meaning |
|---|---|
null |
a published value |
severe_scarcity_code |
the source’s code for severe scarcity: raw 9999, score 5; read it as a code |
arid_low_water_use |
an arid, low-use basin: raw and score are placeholders |
no_data |
not published for this basin |
score_not_published |
the raw value is published without a score (groundwater table decline) |
/api/v1/water-stress/batch/A batch of sites, synchronous, body {"locations": [{lat, lon, label}], "dry_run": false}.
Response Field Reference
| Field path | Type | Meaning |
|---|---|---|
unit.string_id, country, province, area_km2 |
mixed | the basin unit the coordinate sits in |
unit.match.method / gap_m |
string / integer | contains or nearest, and the distance when nearest |
baseline_annual.indicators.{code} |
object | name, raw, score, cat, label, flag for each of 13 indicators |
baseline_annual.profiles.{code} |
object | the ten industry profiles: quantity, quality, regulatory and overall risk |
baseline_monthly.months[] |
array | 12 entries, each with stress, depletion and variability |
future_annual.pathways.{opt,bau,pes}.horizons.{2030,2050,2080} |
object | projected stress, depletion, variability and seasonal variability, plus supply and withdrawal |
audit |
object | flags, arid and no-data markers, the score formula and its check, counts of flagged cells |
Code Examples
curl "https://api.continuuiti.com/api/v1/water-stress/query/?lat=28.6139&lon=77.2090&label=site-01" \
-H "X-API-Key: YOUR_API_KEY"
import requests
API = "https://api.continuuiti.com/api/v1"
H = {"X-API-Key": "YOUR_API_KEY"}
ws = requests.get(f"{API}/water-stress/query/", headers=H,
params={"lat": 28.6139, "lon": 77.2090, "label": "site-01"}).json()
bws = ws["baseline_annual"]["indicators"]["bws"]
if bws["flag"] is None:
print(f"baseline water stress: {bws['label']} (score {bws['score']:.1f})")
bau_2050 = ws["future_annual"]["pathways"]["bau"]["horizons"]["2050"]["ws"]
print("2050, business as usual:", bau_2050["label"])
batch = requests.post(f"{API}/water-stress/batch/", headers=H,
json={"locations": [{"lat": 28.6139, "lon": 77.2090, "label": "site-01"},
{"lat": 38.627, "lon": -90.1994, "label": "site-02"}]}).json()
for item in batch["results"]:
print(item["label"], item["status"])
const API = "https://api.continuuiti.com/api/v1";
const headers = { "X-API-Key": "YOUR_API_KEY" };
const params = new URLSearchParams({ lat: "28.6139", lon: "77.2090", label: "site-01" });
const ws = await (await fetch(`${API}/water-stress/query/?${params}`, { headers })).json();
console.log(ws.baseline_annual.indicators.bws.label);
Sandbox
?dry_run=true on the GET, or "dry_run": true in the batch body, returns a deterministic mock seeded by the coordinate, free, with method_version ending in _dry_run and synthetic unit ids.
Damage Estimation API
The Continuuiti Damage Estimation API computes flood damage estimates for buildings using two independent depth-damage models — one tuned to US construction practices and one tuned to international (continental) construction. Returns monetary loss estimates based on flood depth, building occupancy type, structural characteristics, and replacement value. Synchronous: sub-millisecond per building, 5,000 buildings in under 5 seconds.
33 Building Occupancy Types
Buildings are classified by occupancy code. Pass the code in the occupancy field of damage estimation requests.
| Category | Codes |
|---|---|
| Residential | RES1, RES2, RES3A–RES3F, RES4, RES5, RES6 |
| Commercial | COM1–COM10 |
| Industrial | IND1–IND6 |
| Agriculture | AGR1 |
| Religious | REL1 |
| Government | GOV1, GOV2 |
| Education | EDU1, EDU2 |
Flood Zone Types
| Flood zone | Description |
|---|---|
riverine |
Inland river / fluvial flooding |
coastal_a |
Coastal flood zone (moderate wave action) |
coastal_v |
Coastal flood zone (high-velocity wave action) |
Endpoints
/api/v1/damage/estimate/Compute flood damage for a single building. Returns both US and international model results independently in one response. Provide US-model fields (stories_int, basement, flood_zone, replacement_value) and/or international-model fields (country_iso, floor_area_m2); at least one group required. When you provide stories_int, the other US-model fields (basement, flood_zone, and replacement_value) are required with it.
{
"status": "COMPLETED",
"input": {"depth_ft": 5.0, "occupancy": "RES1", "stories_int": "2", "basement": false,
"flood_zone": "riverine", "replacement_value": 350000,
"country_iso": "USA", "floor_area_m2": 150},
"hazus": {
"damage_ratio": 0.20, "contents_ratio": 0.28,
"structural_loss": 70000.00, "contents_loss": 49000.00,
"total_loss": 119000.00, "depth_in_structure_ft": 4.0
},
"jrc": {
"damage_ratio": 0.6265, "total_loss_eur": 77901.57,
"continent": "north_america", "sector": "residential",
"depth_m": 1.2192, "fallback_used": false
}
}
/api/v1/damage/batch/estimate/Process up to 5,000 buildings in a single request. Body: buildings array (each with the single-estimate fields plus optional label) and optional dry_run.
Response Field Reference
| Field path | Type | Meaning |
|---|---|---|
status |
string | COMPLETED (both models), PARTIAL (one failed), FAILED (both failed) |
hazus.damage_ratio |
number (0–1) | Structural damage as fraction of replacement value |
hazus.contents_ratio |
number (0–1) | Contents damage as fraction of contents value |
hazus.structural_loss |
number | Structural loss in input currency |
hazus.contents_loss |
number | Contents loss in input currency |
hazus.total_loss |
number | Sum of structural and contents loss |
hazus.depth_in_structure_ft |
number | Depth above first floor (depth_ft minus first_floor_height_ft) |
hazus.clamped |
boolean | True if depth exceeded curve range and was clamped |
jrc.damage_ratio |
number (0–1) | Damage as fraction of max sector damage |
jrc.total_loss_eur |
number | Total loss in EUR (2010 base year) |
jrc.depth_m |
number | Flood depth in meters (depth_ft × 0.3048) |
jrc.continent |
string | Continent used for the curve |
jrc.sector |
string | Sector used for the curve |
jrc.fallback_used |
boolean | True if continent/country curve unavailable, global fallback applied |
depth_ft input means flood depth above grade. The engine subtracts the first-floor-height before looking up the damage curve. Defaults: basement = 4.0 ft, RES2 = 3.0 ft, all others (slab-on-grade) = 1.0 ft. Override per-building with first_floor_height_ft.Code Examples
curl -X POST https://api.continuuiti.com/api/v1/damage/estimate/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"depth_ft": 5.0, "occupancy": "RES1",
"stories_int": "2", "basement": false, "flood_zone": "riverine",
"replacement_value": 350000,
"country_iso": "USA", "floor_area_m2": 150
}'
import requests
response = requests.post(
"https://api.continuuiti.com/api/v1/damage/estimate/",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"depth_ft": 5.0, "occupancy": "RES1",
"stories_int": "2", "basement": False, "flood_zone": "riverine",
"replacement_value": 350000,
"country_iso": "USA", "floor_area_m2": 150,
},
)
result = response.json()
print(f"US model loss: ${result['hazus']['total_loss']:,.2f}")
print(f"International loss: EUR {result['jrc']['total_loss_eur']:,.2f}")
const response = await fetch("https://api.continuuiti.com/api/v1/damage/estimate/", {
method: "POST",
headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
depth_ft: 5.0, occupancy: "RES1",
stories_int: "2", basement: false, flood_zone: "riverine",
replacement_value: 350000,
country_iso: "USA", floor_area_m2: 150,
}),
});
const result = await response.json();
Hazard History API
The Continuuiti Hazard History API queries historical flood and landslide events at any location. Flood queries return clustered episodes with monthly seasonality and precipitation thresholds; landslide queries return individual events with year trend and fatality aggregation. Single-location queries are synchronous (HTTP 201) and return in 1 to 3 seconds; batch endpoints are asynchronous and accept up to 1,000 locations each.
Flood Stats Fields
| Field | Meaning |
|---|---|
episodes |
Number of clustered independent flood episodes (7-day gap rule) |
reports |
Total raw events within radius (pre-clustering) |
recurrence_months |
Average months between episodes (observation span / episodes) |
mean_duration_days |
Mean episode duration in days |
peak_month |
Month of year (1–12) with the most episodes |
largest_event_km2 |
Areal extent of the largest single event |
largest_event_date |
Date of the largest event |
Landslide Stats Fields
| Field | Meaning |
|---|---|
total_events |
Number of landslide events within radius |
avg_per_year |
Span-based average (events / total span years) |
years_with_events |
Distinct years with at least one event |
most_active_year |
Year with the most events |
trend |
increasing, decreasing, or stable (first-half vs second-half comparison) |
total_fatalities |
Sum of fatalities across reported events |
Endpoints
/api/v1/hazard-history/floods/query/Query flood history at a single location. Returns clustered episodes, monthly seasonality, precipitation thresholds matched to episode dates, and individual events.
| Param | Type | Default | Range | Description |
|---|---|---|---|---|
lat |
float | required | -90 to 90 | Latitude |
lon |
float | required | -180 to 180 | Longitude |
radius_km |
float | 10 | fixed | Search radius in kilometers. Fixed for now; not yet an adjustable request parameter |
limit |
integer | 5000 | fixed | Maximum events per location. Fixed at 5,000 for now; not yet an adjustable request parameter (stats computed on the full set) |
dry_run |
boolean | false | — | Return deterministic mock data |
{
"query": {"lat": 29.76, "lon": -95.37, "radius_km": 10.0},
"stats": {
"episodes": 172, "reports": 678, "recurrence_months": 1.5,
"mean_duration_days": 2.8, "peak_month": 5,
"largest_event_km2": 1863.6, "largest_event_date": "2005-09-24"
},
"month_counts": {"1":12,"2":12,"3":13,"4":15,"5":23,"6":16,"7":13,"8":18,"9":21,"10":14,"11":7,"12":8},
"flood_thresholds": {
"matched_episodes": 172,
"peak_daily_mm": {"p25": 11.3, "p50": 25.1, "p75": 44.5},
"max_3day_mm": {"p25": 19.2, "p50": 38.7, "p75": 74.2},
"precip_7day_mm":{"p25": 14.9, "p50": 31.2, "p75": 57.0}
},
"events": [
{"lat": 29.7597, "lon": -95.368, "area_km2": 0.02,
"start_date": "2017-08-27", "end_date": "2017-08-30",
"duration_days": 3, "distance_km": 0.2, "year": 2017}
]
}
/api/v1/hazard-history/floods/batch/Batch flood history query for up to 1,000 locations. Returns 202 Accepted with a batch_id; retrieve results from the export endpoints once processing completes (typically a few minutes). Each location costs one credit, and the stored report bundles both flood and landslide history.
| Field | Type | Default | Description |
|---|---|---|---|
locations |
array | required | 1–1,000 locations |
locations[].lat |
float | required | Latitude |
locations[].lon |
float | required | Longitude |
locations[].label |
string | "" |
Optional label |
locations[].radius_km |
float | — | Per-location override |
radius_km |
float | 10 | Default radius for all locations |
limit |
integer | 50 | Max events per location |
dry_run |
boolean | false | Return mock data |
/api/v1/hazard-history/landslides/query/Query landslide history at a single location. Returns events, year trend, and fatality aggregation. Search radius is fixed at 25 km for now (not yet an adjustable request parameter).
{
"query": {"lat": 19.08, "lon": 72.88, "radius_km": 25.0},
"stats": {
"total_events": 13, "avg_per_year": 1.4, "years_with_events": 6,
"most_active_year": 2010, "most_active_year_count": 6,
"trend": "decreasing", "total_fatalities": 24
},
"year_counts": {"2007": 1, "2008": 0, "2009": 1, "2010": 6},
"events": [
{"lat": 19.069, "lon": 72.8703, "date": "2010-06-25",
"trigger": "downpour", "category": "landslide", "size": "medium",
"fatalities": 0, "distance_km": 1.6, "year": 2010}
]
}
/api/v1/hazard-history/landslides/batch/Batch landslide history query for up to 1,000 locations. Same body shape as the flood batch with default radius_km = 25, and the same asynchronous behavior: a 202 Accepted with a batch_id, then results from the export endpoints. One credit per location, and the stored report bundles both hazards.
Response Field Reference
| Field path | Type | Meaning |
|---|---|---|
stats.episodes (flood) |
integer | Clustered independent episodes within radius |
stats.recurrence_months (flood) |
number | Observation-span / episode-count |
flood_thresholds.{metric}.{p25|p50|p75} |
number | Precipitation percentile thresholds matched to episode dates |
month_counts.{1-12} |
integer | Episode counts by start month (seasonality fingerprint) |
stats.trend (landslide) |
string | increasing, decreasing, or stable |
year_counts.{year} |
integer | Event count per year (includes years with zero events) |
events[].distance_km |
number | Distance from query coordinate |
Code Examples
curl "https://api.continuuiti.com/api/v1/hazard-history/floods/query/?lat=29.76&lon=-95.37&radius_km=10" \
-H "X-API-Key: YOUR_API_KEY"
import requests
response = requests.get(
"https://api.continuuiti.com/api/v1/hazard-history/floods/query/",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"lat": 29.76, "lon": -95.37, "radius_km": 10},
)
data = response.json()
print(f"Episodes: {data['stats']['episodes']}, Peak month: {data['stats']['peak_month']}")
const url = new URL("https://api.continuuiti.com/api/v1/hazard-history/floods/query/");
url.searchParams.set("lat", "29.76");
url.searchParams.set("lon", "-95.37");
url.searchParams.set("radius_km", "10");
const response = await fetch(url, { headers: { "X-API-Key": "YOUR_API_KEY" } });
const data = await response.json();
Frequently Asked Questions
What is the Continuuiti API base URL?
The Continuuiti API base URL is https://api.continuuiti.com/api/v1/. All endpoints across the Continuuiti APIs (Geocoder, LULC, Climate Risk, CDEM, Pluvial and Flash Flood, Flood Field, Water Stress, Damage Estimation, Hazard History) are reached by appending the endpoint path to this base URL. For example, climate risk assessment for a single location uses POST https://api.continuuiti.com/api/v1/climate-risk/analyze/.
How do I get a Continuuiti API key?
Sign up for an account at tools.continuuiti.com/accounts/api-docs/ and generate an API key from your dashboard. Every request requires the key in the X-API-Key header. Keys are scoped per account; rate limits apply per key. There is no separate developer plan: every signup gets API access immediately.
Is there an API sandbox or free testing mode?
Yes. Every endpoint that supports dry_run accepts a dry_run query parameter (GET) or body field (POST). Sandbox responses return deterministic mock data — same input always returns the same output — so you can integrate, write tests, and run CI without consuming API quota. Sandbox results are marked so you cannot accidentally treat them as real. See the Sandbox section for details.
What is the difference between sync and async Continuuiti APIs?
Synchronous APIs (Geocoder, Damage Estimation, Hazard History, Water Stress) return results in a single request-response cycle in seconds or sub-seconds. Asynchronous APIs (LULC, Climate Risk, CDEM, Pluvial and Flash Flood, Flood Field) require a job-poll-fetch pattern: POST submits the job and returns a 202 with a job_id, GET polls status, GET fetches results when status is COMPLETED. All async APIs share the same flow and batch pattern, with per-API status variations listed in the Batch & Async section.
What are the Continuuiti API rate limits?
Rate limits apply per API key. Default limits are 60 requests per minute and 500 requests per hour per account; exceeding them returns 429 with a Retry-After header. Batch endpoints accept up to 5,000 locations per submission. For sustained high-volume workloads or custom limits, book a demo to discuss your requirements.
How long do batch jobs take?
Batch processing time depends on the API and batch size. Geocoding completes 10,000 addresses in approximately ten minutes. Climate Risk batches run 2 to 4 minutes per location with controlled concurrency (max 20 parallel jobs). LULC batches run 4 to 6 minutes per location. A Flood Field site takes 30 to 60 seconds. A two-storm pluvial job takes one to two minutes, and a sixteen-storm flash flood set takes three to six. Damage Estimation is sub-millisecond per building.
What happens if a Continuuiti batch partially fails?
When some locations in a batch fail and others succeed, the batch reaches PARTIAL status (not FAILED). The progress object reports counts for completed, failed, and retrying jobs separately. Failed locations can be retried at no additional credit cost. Use the results endpoint with ?status=FAILED to filter just the failed items for retry.
Are Continuuiti APIs aligned with TCFD or IFRS S2 reporting?
Continuuiti APIs provide screening-level data aligned with TCFD physical risk disclosure categories and IFRS S2 implementation guidance. The Climate Risk API outputs map directly to TCFD’s physical risk taxonomy (acute and chronic hazards, multiple scenarios, multiple time horizons). For full regulatory disclosure preparation, see /methodology/climate-risk/ and consult your compliance team.
