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.app/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.app/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.app/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.app/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.
Request
curl -s -X POST "https://api.travelmode.app/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.app/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.app/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
}
}
Webhooks
All /v1/webhooks endpoints (and the attempts endpoint) require the
weather:webhooks scope. See webhooks.md for
signing details and event types.
Create a webhook
POST /v1/webhooks · Bills: 1 unit
The response includes a secret once (whsec_<32 hex chars>).
Use it to verify the X-Travelmode-Signature header on every
delivery. Subsequent reads only return the public secret_prefix.
Request
curl -s -X POST "https://api.travelmode.app/v1/webhooks" \
-H "Authorization: Bearer tm_weather_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/weather",
"events": [
"weather.change_detected",
"weather.severe_alert_added",
"weather.watch_expired",
"weather.watch_failed",
"weather.snapshot_refreshed",
"weather.webhook_delivery_failed",
"weather.webhook_endpoint_disabled"
],
"description": "Production weather notifier"
}'
Response (201 Created)
{
"data": {
"webhook_id": "9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b",
"url": "https://example.com/hooks/weather",
"events": [
"weather.change_detected",
"weather.severe_alert_added",
"weather.watch_expired",
"weather.watch_failed",
"weather.snapshot_refreshed",
"weather.webhook_delivery_failed",
"weather.webhook_endpoint_disabled"
],
"description": "Production weather notifier",
"status": "active",
"secret": "whsec_2c5b1f7e8b3d4f5a6c7d8e9f0a1b2c3d",
"secret_prefix": "whsec_2c5b1f7e8b3d",
"failure_count": 0,
"last_delivery_at": null,
"last_success_at": null,
"last_failure_at": null,
"disabled_at": null,
"created_at": "2026-04-25T18:01:13.500Z",
"updated_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
}
}
List webhook endpoints
GET /v1/webhooks · Bills: 0 units
Returns every endpoint the calling developer owns, including
disabled ones. Secrets are never returned — only the public
secret_prefix.
Request
curl -s "https://api.travelmode.app/v1/webhooks" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"webhooks": [
{
"webhook_id": "9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b",
"url": "https://example.com/hooks/weather",
"events": [
"weather.change_detected",
"weather.severe_alert_added"
],
"description": "Production weather notifier",
"status": "active",
"secret_prefix": "whsec_2c5b1f7e8b3d",
"failure_count": 0,
"last_delivery_at": "2026-04-25T17:55:13.000Z",
"last_success_at": "2026-04-25T17:55:13.000Z",
"last_failure_at": null,
"disabled_at": null,
"created_at": "2026-04-25T18:01:13.500Z",
"updated_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": 0
}
}
Get one webhook endpoint
GET /v1/webhooks/{webhook_id} · Bills: 0 units
Returns one endpoint owned by the calling developer (404 if not
owned).
Request
curl -s "https://api.travelmode.app/v1/webhooks/9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"webhook_id": "9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b",
"url": "https://example.com/hooks/weather",
"events": [
"weather.change_detected",
"weather.severe_alert_added"
],
"description": "Production weather notifier",
"status": "active",
"secret_prefix": "whsec_2c5b1f7e8b3d",
"failure_count": 0,
"last_delivery_at": "2026-04-25T17:55:13.000Z",
"last_success_at": "2026-04-25T17:55:13.000Z",
"last_failure_at": null,
"disabled_at": null,
"created_at": "2026-04-25T18:01:13.500Z",
"updated_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": 0
}
}
Disable a webhook endpoint
DELETE /v1/webhooks/{webhook_id} · Bills: 1 unit
Soft-disables the endpoint (status='disabled',
disabled_at=now). The delivery worker stops targeting it; the row
itself is retained so historical attempts stay queryable.
Request
curl -s -X DELETE "https://api.travelmode.app/v1/webhooks/9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"webhook_id": "9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b",
"status": "disabled",
"disabled_at": "2026-04-25T18:30:00.000Z"
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:30:00.500Z",
"environment": "production",
"billing_units": 1
}
}
List delivery attempts
GET /v1/webhooks/{webhook_id}/attempts?limit=50 · Bills: 0 units
Returns the most-recent delivery attempts for a webhook. Each
attempt carries response status, response time, response body
excerpt, the signature/timestamp pair we sent, and the schedule
timeline. limit defaults to 50 and is capped at 200.
Request
curl -s "https://api.travelmode.app/v1/webhooks/9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b/attempts?limit=10" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"webhook_id": "9f8df5f0-8d9f-4a37-9c1a-9b2b3c4d5e6b",
"attempts": [
{
"attempt_id": "aa00aa00-0000-4000-8000-000000000001",
"event_id": "ee00ee00-0000-4000-8000-000000000001",
"event_type": "weather.change_detected",
"attempt_number": 1,
"status": "delivered",
"response_status": 200,
"response_time_ms": 142,
"response_body_excerpt": "{\"ok\":true}",
"error_message": null,
"request_signature": "9d3e7c1a4b5f6e2d8c0a1b3e5d7f9c2a4b6e8d0f1a3c5e7b9d1f3a5c7e9b1d3f",
"request_timestamp": "2026-04-25T17:55:13.000Z",
"scheduled_at": "2026-04-25T17:55:12.000Z",
"attempted_at": "2026-04-25T17:55:13.000Z",
"completed_at": "2026-04-25T17:55:13.142Z",
"next_retry_at": null
},
{
"attempt_id": "aa00aa00-0000-4000-8000-000000000002",
"event_id": "ee00ee00-0000-4000-8000-000000000002",
"event_type": "weather.severe_alert_added",
"attempt_number": 2,
"status": "retrying",
"response_status": 503,
"response_time_ms": 1240,
"response_body_excerpt": "Service Unavailable",
"error_message": "Upstream returned 503",
"request_signature": "1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a3c5e7b9d1f3a",
"request_timestamp": "2026-04-25T17:50:13.000Z",
"scheduled_at": "2026-04-25T17:50:12.000Z",
"attempted_at": "2026-04-25T17:50:13.000Z",
"completed_at": "2026-04-25T17:50:14.240Z",
"next_retry_at": "2026-04-25T17:55:14.240Z"
}
]
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:00:00.500Z",
"environment": "production",
"billing_units": 0
}
}
Get usage
GET /v1/usage · Scope: any valid key · Bills: 0 units
Returns the calling key's plan, environment, current-period bounds, plan limits, and current usage + remaining for every quota dimension. Calling this endpoint never burns request quota and is explicitly excluded from rate-limit enforcement.
Request
curl -s "https://api.travelmode.app/v1/usage" \
-H "Authorization: Bearer tm_weather_..."
Response (200 OK)
{
"data": {
"plan": "free",
"environment": "production",
"is_internal": false,
"period": {
"start": "2026-04-01T00:00:00.000Z",
"end": "2026-05-01T00:00:00.000Z"
},
"limits": {
"requests_per_month": 1000,
"timeline_items_per_month": 100,
"active_watches": 0,
"route_calls_per_month": 0
},
"usage": {
"requests": { "limit": 1000, "used": 42, "remaining": 958 },
"timeline_items": { "limit": 100, "used": 6, "remaining": 94 },
"route_calls": { "limit": 0, "used": 0, "remaining": 0 },
"active_watches": { "limit": 0, "used": 0, "remaining": 0 }
}
},
"meta": {
"request_id": "7c0f3c2c-9e4a-4d29-b4a2-9c2b4e9f5c11",
"generated_at": "2026-04-25T18:01:13.500Z",
"environment": "production",
"billing_units": 0
}
}
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.app/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
}
}