Home › API Documentation

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.

Developer Access
Get a Continuuiti API Key
Every Continuuiti Tools account includes a personal API key. Create an account, copy your key from the Platform API page, and pass it in the X-API-Key header.

Get API Key

Getting Started

Get your API key

Every Continuuiti Tools account includes a personal API key. To get yours:

  1. Create an account at tools.continuuiti.com with your business email.
  2. Open Platform API in the sidebar. Your key (sk_live_...) is generated automatically, so copy it from there.
  3. Pass it on every request in the X-API-Key header.

You can regenerate the key at any time from the same page. The old key stops working immediately.

Credits. Triggering analyses consumes the same per-module credits as the web dashboard (1 credit per location or building; pluvial and flash flood runs cost 1 credit per storm). If your balance is insufficient the API returns 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/.
Your jobs, your dashboard. Every analysis you submit through the API is recorded against your account and appears in your web dashboard alongside jobs submitted through the UI. Jobs complete automatically on our side, so you can poll the status endpoints but you never have to.

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
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
curl https://api.continuuiti.com/api/v1/exports/geocoding/reports/ \
  -H "X-API-Key: YOUR_API_KEY"
Python
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()
Node
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"}
Best practice: Store your API key in an environment variable, never in source code. Rotate keys periodically from the dashboard. Treat keys as sensitive credentials — anyone with the key can make billable calls against your account.

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 lulc credits.
  • 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

  1. POST to /{api}/batch/analyze/ with your locations array. Returns 202 Accepted with a batch_id immediately — processing has not started yet.
  2. GET /{api}/batch/{batch_id}/ repeatedly to poll progress. Status moves through PROCESSING and ends at COMPLETED, PARTIAL, or FAILED.
  3. GET /{api}/batch/{batch_id}/results/?page=1&page_size=100 to 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.

Python (full polling loop)
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.

GET/api/v1/exports/{module}/reports/

List your reports for a module (paginated and filterable).

GET/api/v1/exports/{module}/reports/{id}/

Fetch one full report with its results.

GET/api/v1/exports/{module}/batches/

List your batches for a module.

GET/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 (geocoder dry-run)
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
  }'
Python (climate risk dry-run)
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())
Node (hazard history dry-run)
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},
      ...
    },
    ...
  }
}
When to use sandbox: writing tests against the API contract; building frontend code without consuming quota; demoing the platform to stakeholders without quota concerns; CI pipelines that exercise the integration path without budget impact. Sandbox does not validate your inputs against the real data — production calls are still required for real assessments.

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."
}
503 handling: Async APIs already retry transient upstream failures automatically (see Batch & Async — retry behavior). For sync APIs, retry once after 2 seconds; if still 503, retry once more after 8 seconds. If the third attempt fails, treat it as a real outage.

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.

Practical guidance: Batch endpoints (max 5,000 locations per submission) drain at controlled concurrency on the server side — you do not need to throttle your own batch submissions. For sync endpoints (Geocoder single, Damage Estimation, Hazard History), keep client-side concurrency at 5–10 parallel requests per key as a safe baseline.

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

POST/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
GET/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
GET/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"}
    },
    ...
  }
}
POST/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
GET/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/"
}
GET/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
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
  }'
Python
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"]
Node
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.”

Python (geocode → climate risk → top hazards)
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

POST/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.

GET/api/v1/exports/geocoding/reports/{id}/

Retrieve a specific previously geocoded address by ID through the export API. Returns 404 if not found.

POST/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
GET/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.

GET/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
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"}'
Python
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"])
Node
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

POST/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)
GET/api/v1/lulc/jobs/{job_id}/

Get job status. Status values: PENDING, RUNNING, COMPLETED, FAILED. Progress object reports years_completed / years_total.

GET/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}]
  }
}
POST/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.

GET/api/v1/lulc/batch/{batch_id}/

Get batch status and progress. Same shape as the cross-API batch status pattern.

GET/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
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}'
Python
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"]
Node
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.

POST/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
GET/api/v1/cdem/jobs/{job_id}/

Job status with per-layer progress.

GET/api/v1/cdem/results/{job_id}/

Summary with bundle_url and per-layer status.

POST/api/v1/cdem/batch/analyze/

Submit a batch of sites.

GET/api/v1/cdem/batch/{batch_id}/

Batch status (standard pattern).

GET/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
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"}'
Python
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
Node
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

POST/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.

GET/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.

GET/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.

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

GET/api/v1/pluvial/batch/{batch_id}/

Batch status.

GET/api/v1/pluvial/batch/{batch_id}/results/

Paginated per-site summaries.

GET/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
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]
  }'
Python
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
Node
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.

Python (terrain, storms, reading)
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

POST/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.

GET/api/v1/flood-field/jobs/{job_id}/

Job status with per-layer progress.

GET/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.

POST/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.

GET/api/v1/flood-field/batch/{batch_id}/

Batch status (standard pattern).

GET/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
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"}'
Python
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
Node
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

GET/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)
POST/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
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"
Python
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"])
Node
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

POST/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
  }
}
POST/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
First Floor Height (FFH): The 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
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
  }'
Python
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}")
Node
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.

One call covers both hazards. Each query costs one credit, and the stored report includes both flood and landslide history regardless of which endpoint you call, so a single call per location is enough.

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

GET/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}
  ]
}
POST/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
GET/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}
  ]
}
POST/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
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"
Python
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']}")
Node
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.

Continuuiti Platform
Try the Platform Yourself
Run climate risk, flood, water stress, geocoding, and damage assessments across up to 5,000 locations per batch. Start with a demo.

Book a Demo