Endpoints
Last verified: 2026-05-03 (Task #334 — docs accuracy pass).
Reference for every Weather Intelligence endpoint. The OpenAPI spec
in openapi.yaml is the source of truth for full
schemas — the sections here cover the human-readable summary, the
required scope, and at least one paired request + response
example per endpoint.
Response envelope
Every successful response returns:
{
"data": <endpoint-specific payload>,
"meta": {
"request_id": "uuid",
"generated_at": "iso-8601",
"environment": "production",
"cache_status": "HIT|MISS|STALE|MIXED",
"snapshot_id": "uuid",
"billing_units": 1,
"items_analyzed": 1,
"segments_analyzed": 0,
"units": "metric",
"language": "en"
}
}
meta.cache_status, meta.snapshot_id, meta.items_analyzed, and
meta.segments_analyzed are populated only on the endpoints they
make sense for. Errors share the same meta block but ship the
payload under error instead of data (see
errors.md).
Standard headers on every response
| Header | Notes |
|---|---|
X-Request-Id | Same value as meta.request_id. |
X-Cache-Status | Same value as meta.cache_status (when applicable). |
X-Weather-Snapshot-Id | Same value as meta.snapshot_id (when applicable). |
X-RateLimit-Limit | Cap for the requests quota dimension on the calling plan. |
X-RateLimit-Remaining | Requests left in the current monthly window. |
X-RateLimit-Reset | Unix-seconds when the monthly window rolls over. |
Current weather
GET /v1/weather/current · Scope: weather:read · Bills: 1 unit
Query parameters
| Name | Required | Notes |
|---|---|---|
lat | yes | -90 to 90 |
lng | yes | -180 to 180 |
units | no | metric (default), imperial, standard |
language | no | BCP-47 (en, en-GB, …) |
activity_type | no | Free-form. Unknown values fall back to the outdoor default profile. |
Request
curl -s "https://api.travelmode.ai/v1/weather/current?lat=48.8566&lng=2.3522" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"location": {
"lat": 48.8566, "lng": 2.3522,
"geohash": "u09tu", "geohash_precision": 5,
"timezone": "Europe/Paris", "place_type": "city"
},
"current": {
"observed_at": "2026-04-25T18:00:00.000Z",
"temperature": 14.2, "feels_like": 12.9,
"humidity_pct": 71, "pressure_hpa": 1013,
"wind": { "speed_ms": 4.1, "gust_ms": 7.2, "direction_deg": 220 },
"cloud_cover_pct": 75, "visibility_m": 10000,
"uv_index": 1, "precipitation_mm": 0.2,
"condition": { "code": "500", "label": "light rain", "icon": "10d" }
},
"alerts": [],
"risk": { "level": "low", "reasons": [], "recommendation": null, "activity_type": null },
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"provider": "openweather",
"provider_version": "onecall/3.0",
"attribution": "Weather data provided by OpenWeather",
"fetched_at": "2026-04-25T18:01:13.412Z"
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:01:13.500Z",
"environment": "production",
"cache_status": "HIT",
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"billing_units": 1,
"units": "metric",
"language": "en"
}
}
Forecast
GET /v1/weather/forecast · Scope: weather:read · Bills: 1 unit (daily) / 2 units (hourly)
Query parameters
| Name | Required | Notes |
|---|---|---|
lat | yes | |
lng | yes | |
days | no | 1–8 (default 7). Clamped to provider max. |
granularity | no | daily (default) or hourly. |
units, language | no | Same as current. |
Request
curl -s "https://api.travelmode.ai/v1/weather/forecast?lat=48.8566&lng=2.3522&days=3&granularity=daily" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"location": { "lat": 48.8566, "lng": 2.3522, "geohash": "u09tu", "geohash_precision": 5, "timezone": "Europe/Paris", "place_type": "city" },
"granularity": "daily",
"days": 3,
"hourly": null,
"daily": [
{
"local_date": "2026-04-26",
"sunrise": "2026-04-26T04:55:00.000Z",
"sunset": "2026-04-26T18:55:00.000Z",
"temperature": { "min": 8.1, "max": 16.4 },
"feels_like": { "min": 6.9, "max": 15.2 },
"humidity_pct": 65,
"wind": { "speed_ms": 3.2, "gust_ms": 6.1, "direction_deg": 200 },
"cloud_cover_pct": 40,
"precipitation_mm": 1.2,
"precipitation_probability_pct": 35,
"uv_index_max": 5,
"condition": { "code": "500", "label": "light rain", "icon": "10d" }
}
],
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"provider": "openweather",
"provider_version": "onecall/3.0",
"attribution": "Weather data provided by OpenWeather",
"fetched_at": "2026-04-25T18:01:13.412Z"
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:01:13.500Z",
"environment": "production",
"cache_status": "HIT",
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"billing_units": 1,
"items_analyzed": 3,
"units": "metric",
"language": "en"
}
}
The daily array is shown trimmed to one entry for brevity — a real
days=3 response carries three entries (and days=7 carries
seven).
Timeline
POST /v1/weather/timeline · Scope: weather:timeline · Bills: 2 units × items_analyzed
Accepts up to 100 items per call. Each item gets weather + risk;
the response also carries an aggregate summary (overall risk, main
issue, best outdoor window).
activity_type is free-form. Canonical MVP values:
walking, walking_tour, hiking, beach, dining,
outdoor_dining, museum, shopping, driving, train, ferry,
cycling, airport_transfer, event, sightseeing, free_time.
Request
curl -s -X POST "https://api.travelmode.ai/v1/weather/timeline" \
-H "Authorization: Bearer tm_weather_..." \
-H "Content-Type: application/json" \
-d '{
"units": "metric",
"language": "en",
"items": [
{
"id": "morning_walk",
"start": "2026-05-15T08:00:00.000Z",
"end": "2026-05-15T09:30:00.000Z",
"lat": 48.8566, "lng": 2.3522,
"activity_type": "walking_tour",
"timezone": "Europe/Paris"
},
{
"id": "louvre_visit",
"start": "2026-05-15T10:00:00.000Z",
"end": "2026-05-15T13:00:00.000Z",
"lat": 48.8606, "lng": 2.3376,
"activity_type": "museum",
"timezone": "Europe/Paris"
}
]
}'
Response (200 OK)
{
"data": {
"items": [
{
"id": "morning_walk",
"activity_type": "walking_tour",
"outdoor": true,
"start": "2026-05-15T08:00:00.000Z",
"weather": {
"observed_at": "2026-05-15T08:00:00.000Z",
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"cache_status": "HIT",
"temperature_c": 12.4, "feels_like_c": 11.2,
"condition": "light rain",
"precipitation_probability": 65,
"precipitation_intensity_mm_h": 1.2,
"wind_speed_kph": 14.4, "wind_gust_kph": 22.0,
"uv_index": 2
},
"risk": {
"level": "watch",
"reasons": [
{ "code": "rain_likely", "level": "watch", "message": "Light rain likely (65% chance)" }
],
"recommendation": "Bring a rain jacket; light rain expected during your walking tour."
}
},
{
"id": "louvre_visit",
"activity_type": "museum",
"outdoor": false,
"start": "2026-05-15T10:00:00.000Z",
"weather": {
"observed_at": "2026-05-15T10:00:00.000Z",
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"cache_status": "HIT",
"temperature_c": 13.0, "feels_like_c": 12.0,
"condition": "light rain",
"precipitation_probability": 60,
"precipitation_intensity_mm_h": 0.8,
"wind_speed_kph": 12.6, "wind_gust_kph": 20.5,
"uv_index": 2
},
"risk": { "level": "low", "reasons": [] }
}
],
"summary": {
"overall_risk": "watch",
"main_issue": "Light rain likely (65% chance) for walking tour at 10:00 AM",
"best_outdoor_window": {
"start": "2026-05-15T13:00:00.000Z",
"end": "2026-05-15T17:00:00.000Z",
"level": "low"
}
}
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:01:13.500Z",
"environment": "production",
"cache_status": "HIT",
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"billing_units": 4,
"items_analyzed": 2,
"units": "metric",
"language": "en"
}
}
Validation errors
| Code | When |
|---|---|
bad_request | Top-level body failed schema (missing items, etc). |
timeline_item_invalid | Individual item failed validation. details carries index, field, and issues. |
Route
POST /v1/weather/route · Scope: weather:route · Bills: 3 units × segments_analyzed
Samples points along a route and returns weather + risk per segment,
plus an overall_risk summary.
transport_mode is one of car, bus, train, ferry, walking,
cycling, shuttle, transfer. The optional polyline field
accepts an encoded Google polyline string, an array of [lat, lng]
tuples, or an array of { lat, lng } objects. When omitted, the
analyzer samples a great-circle line between origin and destination.
Request
curl -s -X POST "https://api.travelmode.ai/v1/weather/route" \
-H "Authorization: Bearer tm_weather_..." \
-H "Content-Type: application/json" \
-d '{
"origin": { "lat": 48.8566, "lng": 2.3522 },
"destination": { "lat": 49.2583, "lng": 4.0317 },
"transport_mode": "car",
"start_time": "2026-05-15T12:00:00.000Z",
"units": "metric",
"language": "en"
}'
Response (200 OK)
{
"data": {
"origin": { "lat": 48.8566, "lng": 2.3522 },
"destination": { "lat": 49.2583, "lng": 4.0317 },
"transport_mode": "car",
"segments": [
{
"index": 0,
"start_point": { "lat": 48.8566, "lng": 2.3522 },
"end_point": { "lat": 49.0571, "lng": 3.1920 },
"start_time": "2026-05-15T12:00:00.000Z",
"end_time": "2026-05-15T13:00:00.000Z",
"distance_m": 67000, "duration_minutes": 60,
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"cache_status": "HIT",
"condition": {
"label": "clear", "temperature_c": 17.2,
"wind_speed_kph": 16.0, "wind_gust_kph": 24.0,
"precipitation_mm": 0.0, "visibility_km": 10.0
},
"risk": { "level": "low", "reasons": [], "recommendation": null }
},
{
"index": 1,
"start_point": { "lat": 49.0571, "lng": 3.1920 },
"end_point": { "lat": 49.2583, "lng": 4.0317 },
"start_time": "2026-05-15T13:00:00.000Z",
"end_time": "2026-05-15T14:00:00.000Z",
"distance_m": 76000, "duration_minutes": 60,
"snapshot_id": "8a8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e7a",
"cache_status": "MISS",
"condition": {
"label": "heavy rain", "temperature_c": 14.8,
"wind_speed_kph": 38.0, "wind_gust_kph": 60.0,
"precipitation_mm": 4.5, "visibility_km": 4.0
},
"risk": {
"level": "warning",
"reasons": [
{ "code": "heavy_rain", "level": "warning", "message": "Heavy rain reduces visibility" }
],
"recommendation": "Slow down and consider stopping if visibility drops further."
}
}
],
"overall_risk": {
"level": "warning",
"reasons": [
{ "code": "heavy_rain", "level": "warning", "message": "Heavy rain reduces visibility" }
],
"recommendation": "Slow down and consider stopping if visibility drops further.",
"main_issue": "Heavy rain on the second leg of the drive"
}
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T12:00:13.500Z",
"environment": "production",
"cache_status": "MIXED",
"snapshot_id": "8a8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e7a",
"billing_units": 6,
"segments_analyzed": 2,
"units": "metric",
"language": "en"
}
}
Validation errors
| Code | When |
|---|---|
route_invalid | Body failed schema (bad coords, malformed polyline, unknown transport). |
Watches
All three watch endpoints require the weather:watch scope.
Create a watch
POST /v1/weather/watch · Bills: 1 unit + 1/day per active watch
A watch monitors a place / timeline item / route window / trip.
Whenever the upstream snapshot moves enough to cross one of the
watch's change_rules, a weather.change_detected event is recorded
and dispatched to the configured webhook URL. change_rules is
optional — defaults from PRD §18.2 are merged in for any field you
don't supply.
The webhook_url you set here is POSTed directly with a signed JSON
envelope (no /v1/webhooks registration needed). See
watch-webhooks.md for the headers, the
HMAC-SHA256 signature formula, the payload shape, and the retry/backoff
behaviour you need to verify and trust those calls.
Request
curl -s -X POST "https://api.travelmode.ai/v1/weather/watch" \
-H "Authorization: Bearer tm_weather_..." \
-H "Content-Type: application/json" \
-d '{
"target_type": "place",
"target_ref": { "place_id": "city:paris" },
"latitude": 48.8566,
"longitude": 2.3522,
"place_type": "city",
"activity_type": "walking_tour",
"outdoor": true,
"starts_at": "2026-05-15T00:00:00.000Z",
"ends_at": "2026-05-22T00:00:00.000Z",
"webhook_url": "https://example.com/hooks/weather"
}'
Response (201 Created)
{
"data": {
"watch_id": "1f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6a",
"status": "active",
"target_type": "place",
"target_ref": { "place_id": "city:paris" },
"starts_at": "2026-05-15T00:00:00.000Z",
"ends_at": "2026-05-22T00:00:00.000Z",
"change_rules": {
"precipitation_probability_delta": 30,
"temperature_c_delta": 5,
"wind_kph_delta": 20,
"uv_index_delta": 3,
"detect_severe_alerts": true,
"detect_risk_level_change": true
},
"activity_type": "walking_tour",
"transport_mode": null,
"outdoor": true,
"webhook_url": "https://example.com/hooks/weather",
"next_check_at": "2026-05-15T00:00:00.000Z",
"created_at": "2026-04-25T18:01:13.500Z"
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:01:13.500Z",
"environment": "production",
"billing_units": 1
}
}
Get a watch
GET /v1/weather/watch/{watch_id} · Bills: 1 unit
Returns the watch hydrated with its latest snapshot, latest risk
assessment, and the most recent change events. Returns 404 if the
watch isn't owned by the calling developer account.
Request
curl -s "https://api.travelmode.ai/v1/weather/watch/1f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6a" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"watch_id": "1f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6a",
"status": "active",
"target_type": "place",
"target_ref": { "place_id": "city:paris" },
"latitude": 48.8566,
"longitude": 2.3522,
"starts_at": "2026-05-15T00:00:00.000Z",
"ends_at": "2026-05-22T00:00:00.000Z",
"change_rules": {
"precipitation_probability_delta": 30,
"temperature_c_delta": 5,
"wind_kph_delta": 20,
"uv_index_delta": 3,
"detect_severe_alerts": true,
"detect_risk_level_change": true
},
"activity_type": "walking_tour",
"transport_mode": null,
"outdoor": true,
"webhook_url": "https://example.com/hooks/weather",
"next_check_at": "2026-05-15T00:30:00.000Z",
"last_checked_at": "2026-05-15T00:15:00.000Z",
"last_risk_level": "low",
"failure_count": 0,
"created_at": "2026-04-25T18:01:13.500Z",
"updated_at": "2026-05-15T00:15:00.000Z",
"expired_at": null,
"latest_snapshot": {
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"forecast_type": "current",
"valid_from": "2026-05-15T00:00:00.000Z",
"valid_to": "2026-05-15T01:00:00.000Z",
"fetched_at": "2026-05-15T00:15:00.000Z",
"freshness_band": "current",
"metrics": {
"temperature_c": 14.2,
"precipitation_probability": 25,
"wind_speed_kph": 18.0
},
"alerts": []
},
"latest_risk": {
"risk_assessment_id": "5e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"level": "low",
"recommendation": null,
"reasons": []
},
"recent_change_events": []
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-05-15T00:15:01.500Z",
"environment": "production",
"snapshot_id": "4e8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6f",
"billing_units": 1
}
}
Cancel a watch
DELETE /v1/weather/watch/{watch_id} · Bills: 1 unit
Soft-cancels the watch (status='cancelled', expired_at=now). The
refresh worker stops picking it up. Returns 404 if the watch isn't
owned by the calling developer account.
Request
curl -s -X DELETE "https://api.travelmode.ai/v1/weather/watch/1f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6a" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"watch_id": "1f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6a",
"status": "cancelled",
"expired_at": "2026-05-15T01:00:00.000Z"
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-05-15T01:00:00.500Z",
"environment": "production",
"billing_units": 1
}
}
Health
GET /v1/weather/health · Scope: weather:read · Bills: 0 units
Tiny ping that proves the API surface is reachable and the caller's key + scope check passed. Doesn't depend on the upstream weather provider being warm — safe to call on any frequency.
Request
curl -s "https://api.travelmode.ai/v1/weather/health" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": { "status": "ok", "environment": "production" },
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:01:13.500Z",
"environment": "production",
"billing_units": 0
}
}