# Calchis Enterprise API Guide

**Version:** 1.0
**Base URL:** `https://calchis.com/api/v1`
**Last Updated:** 2026-03-25

---

## Authentication

All API requests require a Bearer token in the `Authorization` header:

```
Authorization: Bearer cs_live_xxxxxxxxxxxxx
```

API keys are provisioned per-organization. Each key is tied to a subscription tier that determines rate limits and feature access.

| Prefix | Environment |
|--------|-------------|
| `cs_live_` | Production |
| `cs_test_` | Sandbox (rate-limited, no billing) |

---

## Rate Limits

| Tier | Requests/min |
|------|-------------|
| Free | 60 |
| Analyst | 300 |
| Agency | 300 |
| Enterprise | 1,000 |

Taken from `TIER_LIMITS` in `middleware.ts`, which is the code that enforces
them. There is no burst allowance and no concurrency cap: the limiter is a
sliding window over one minute, kept per server instance
(`lib/rate-limit/memory-limiter.ts`, since 2026-10-21; Upstash before), so the
only dimension is requests per minute. The daily quota per API key is counted in
Postgres and is the same on every instance.

Rate limit headers are included on every response:

```
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1711389600
X-Request-ID: a1b2c3d4
X-API-Version: 1.0
```

When rate-limited, the API returns `429 Too Many Requests` with a `Retry-After` header (seconds).

---

## Core Endpoints

### Events

| Method | Path | Description |
|--------|------|-------------|
| GET | `/events/live` | All active US events as GeoJSON FeatureCollection |
| GET | `/events/{id}` | Single event detail |
| GET | `/events/{id}/rings` | Consequence ring geometries (GeoJSON); see *Reading the rings* below |
| GET | `/events/{id}/score` | Hazard score for event location |
| GET | `/events/{id}/timeline` | Event status timeline (severity progression) |
| GET | `/events/{id}/exposure` | Population + infrastructure exposure estimates |
| GET | `/triggers/evaluations?event=` | The latest evaluation of each of your triggers against one event, in one request (what the dashboard reads) |

#### Reading the rings

`ring_type` says what a ring **is**; `zone` says how close. The pairing differs by peril, and an integrator who keys on one without the other misreads it (an external consumer did, 2026-11-01):

| Peril | Zone 1 | Zone 2 | Zone 3 | Zone 4 | Separate ring |
|---|---|---|---|---|---|
| Wildfire | NIFC perimeter, else acreage circle (`evacuation`) | +3 km (`evacuation`) | +10 km "Smoke & Ember Zone" (`smoke` — embers travel with it; a fire threat) | +30 km "Air Quality Impact Zone" (`smoke`) | — |
| Hurricane | 64 kt radii by quadrant (`wind`) | 50 kt (`wind`) | 34 kt (`wind`) | outer wind field and rain bands, circular, by category | `surge`, zone 1: potential surge flooding while active; `parameters.source` = `nhc_slosh` (NHC map) or `estimated` (category fallback — show as an estimate or not at all) |
| Earthquake | ShakeMap MMI VIII+ when published, else modeled (BSSA14 → Worden 2012) | MMI VI–VII | MMI IV–V | MMI II–III | — |
| Flood | National Water Model inundation inside the warning (experimental), when present | the NWS warning polygon — the official boundary | circular | circular | — |
| Severe weather | the SPC/NWS polygon is the primary zone (a watch is labeled a watch) | circular buffers outward from the primary zone | | | — |

Wildfire zones 2–4 are circles around the fire centroid: a fixed buffer with no wind direction, not a plume. Fires under 10 acres and FIRMS satellite clusters (no acreage) have no rings. Every ring is **modeled**; what agencies measured afterward is `/events/{id}/footprints` with `kind: observed`. Full calibration: `/docs/consequence-ring-calibration.md`.

### Scoring

| Method | Path | Description |
|--------|------|-------------|
| GET | `/risk/score?lat={lat}&lng={lng}` | Climate-adjusted hazard score by location |
| GET | `/risk/score?lat={lat}&lng={lng}&format=oasis` | OASIS LMF formatted output |

### ILS Triggers

| Method | Path | Description |
|--------|------|-------------|
| GET | `/triggers` | List user's active triggers |
| POST | `/triggers` | Create a new parametric trigger |
| GET | `/triggers/{id}` | Trigger detail |
| PUT | `/triggers/{id}` | Update trigger parameters |
| DELETE | `/triggers/{id}` | Deactivate trigger |
| GET | `/triggers/{id}/history` | Trigger evaluation history |

### Portfolio

| Method | Path | Description |
|--------|------|-------------|
| GET | `/portfolio` | List portfolios |
| POST | `/portfolio` | Create portfolio |
| GET | `/portfolio/{id}/exposure` | Portfolio-level exposure analysis |
| POST | `/portfolio/{id}/locations` | Add locations to portfolio |

---

## Data Formats

### Coordinates
All coordinates are **WGS84 (EPSG:4326)**. Latitude/longitude in decimal degrees.

### Timestamps
All timestamps are **UTC ISO 8601**: `2026-03-25T14:30:00.000Z`

### Spatial Output
All geospatial data is **OGC-compliant GeoJSON** per RFC 7946.

### OASIS LMF Export
Append `?format=oasis` to any scoring endpoint. Returns OASIS Loss Modeling Framework v2.3.2 compatible JSON with:
- `LocNumber`, `AccNumber`, `LocName`, `Latitude`, `Longitude`
- `OccupancyCode`, `ConstructionCode`, `YearBuilt`
- `BuildingTIV`, `ContentsTIV`
- Per-peril `HazardIntensity`, `HazardUnit`, `ScoreComponents`

Directly importable into Moody's RMS RiskLink, Verisk Touchstone/AltaCalc, and other OASIS-compatible tools.

---

## ILS Trigger Types

| Trigger Type | Unit | Applicable Perils |
|-------------|------|-------------------|
| `wind_speed_kt` | Knots | Hurricane |
| `storm_surge_ft` | Feet | Hurricane |
| `magnitude` | Richter | Earthquake |
| `industry_loss_usd` | USD | All |
| `gauge_stage_ft` | Feet | Flood |
| `containment_fail_pct` | % uncontained | Wildfire |

### Trigger Lifecycle

```
MONITORING → APPROACHING (≥alert_at_pct of threshold) → BREACHED → RESOLVED
```

Webhook notifications are sent at each state transition.

---

## Webhook Delivery

Enterprise tier includes webhook notifications for trigger state changes. Webhooks are HMAC-signed for verification.

### Webhook Payload

```json
{
  "event": "trigger.state_changed",
  "trigger_id": "uuid",
  "event_id": "uuid",
  "previous_state": "monitoring",
  "new_state": "approaching",
  "current_value": 95.2,
  "threshold_value": 100.0,
  "pct_of_threshold": 95.2,
  "timestamp": "2026-03-25T14:30:00.000Z"
}
```

### Signature Verification

```
X-Calchis-Signature: sha256=<HMAC-SHA256 of raw body using webhook secret>
```

Verify by computing HMAC-SHA256 of the raw request body using your webhook secret and comparing to the signature header.

### Delivery Guarantees

- At-least-once delivery with exponential backoff retry
- Retry schedule: 30s, 2m, 15m, 1h, 4h (5 attempts)
- Webhook endpoint must respond with 2xx within 10 seconds
- Failed webhooks are logged and available via `/triggers/{id}/history`

---

## Data Freshness

Polling cadence comes from `vercel.json`; the staleness window each source is judged against is twice its interval plus two minutes (`lib/cron-schedule.ts`, `staleAfterMinutes`). This table is a copy of that schedule as of 2026-09-04 — `/api/v1/health` is the live answer.

| Data Source | Polling Interval | Judged Stale After |
|-------------|-----------------|--------------------|
| USGS Earthquakes | Every 5 minutes | 12 minutes |
| NASA FIRMS (wildfire hotspots) | Every 15 minutes | 32 minutes |
| NOAA NHC (hurricanes) | Every 30 minutes | 62 minutes |
| NOAA AHPS (flood gauges) | Every 10 minutes | 22 minutes |
| NOAA SPC (severe weather) | Every 5 minutes | 12 minutes |
| NWS storm alerts | Every 5 minutes | 12 minutes |
| NWS winter alerts | Every 15 minutes | 32 minutes |
| NWS fire weather | Every 30 minutes | 62 minutes |
| NIFC (wildfire perimeters) | Every 12 hours | 24 hours 2 minutes |
| ILS Trigger evaluation | Every 5 minutes | 12 minutes |

Data freshness is monitored via `/api/v1/health` and Sentry alerting. Three consecutive source failures trigger an engineering alert.

---

## Hazard Score Components

Every hazard score includes transparent `score_components` showing how the 0-10 score was computed:

```json
{
  "peril": "hurricane",
  "score": 8.2,
  "score_type": "hazard",
  "components": {
    "track_frequency": { "score": 3.1, "max": 3.5, "label": "CAT3+ historical track frequency" },
    "coastal_proximity": { "score": 2.8, "max": 3.0, "label": "Distance to coastline" },
    "surge_exposure": { "score": 2.3, "max": 3.5, "label": "Storm surge exposure" }
  },
  "parameters": ["uuid-1", "uuid-2", "uuid-3"],
  "model_version": "1.0"
}
```

Each `parameter` UUID traces to the `model_parameters` table with full source attribution.

---

## Error Handling

All errors return JSON with consistent structure:

```json
{
  "error": "Rate limit exceeded",
  "message": "Enterprise tier allows 1000 requests per minute",
  "retry_after_seconds": 12
}
```

| Status | Meaning |
|--------|---------|
| 200 | Success |
| 400 | Bad request (invalid parameters) |
| 401 | Authentication required or invalid |
| 403 | Forbidden (feature not available on your tier) |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
| 500 | Internal server error (reported to Sentry automatically) |

---

## Support

- **Documentation:** https://calchis.com/docs
- **API Status:** https://calchis.com/status
- **Enterprise Support:** enterprise@calchis.com
- **Availability:** Calchis targets 99.9% monthly availability for the Enterprise tier and publishes measured status at https://calchis.com/status. This is a target, not a contractual SLA: no credits are offered unless an SLA is signed as part of an Enterprise agreement.
