Watches v1
GET · POST · PATCH · DELETE https://api.trailsplits.com/v1
Needs your API key
Store a route and a few dates; TrailSplits checks it every few hours and calls your webhook when the best window or a thunderstorm signal changes. Facts and their changes, never “safe”. Manage watches and the signing secret in the API console.
Contract
- Consumers:
A watch is a stored route and date range that TrailSplits re-evaluates on a timer. When what matters changes, it tells you:
- a webhook to your server for API projects;
- a push for TrailSplits users (the first-party bridge).
It reports facts and their changes, never "safe" or "go".
Authentication
Like GET /v1/usage (api-keys-usage-v1):
-
A secret API key: the project is the key's own. Publishable (
ts_pub_) keys get 403scope_denied: watches carry a webhook and stored geometry, so they are server-side only. -
The owner's JWT plus
?project=prj_…: this is what the console uses.
Every answer has Cache-Control: private, no-store.
Objects
Watch
{
"id": "wch_…",
"name": "Tour du Mont Blanc stage 3",
"status": "active",
"from": "2026-10-04",
"to": "2026-10-10",
"timezone": "Europe/Zurich",
"start_times": ["06:00", "07:00", "08:00"],
"events": ["window_changed", "thunderstorm_signal", "closure", "snow"],
"delivery": { "kind": "webhook", "url": "https://example.com/hooks/trailsplits" },
"line": { "points": 1840, "length_km": 18.4 },
"metadata": { "trip": "abc" },
"created_at": "2026-10-03T08:00:00Z",
"ends_at": "2026-10-11T00:00:00Z",
"last_evaluated_at": "2026-10-03T09:00:00Z",
"next_evaluation_at": "2026-10-03T12:00:00Z",
"last_state": { "best_window": { "date": "2026-10-05", "start": "07:00" }, "flags": ["thunderstorm_signal"] }
}
-
statusisactive,paused,ended(pastto) orfailing(webhook deliveries keep failing; see Deliveries). -
The geometry itself is never returned. The answer only says it is stored (
line.points).
Endpoints
| Method and path | Body | Answer |
|---|---|---|
POST /v1/watches | {line, name?, from, to, timezone?, start_times?, pace?, prefer?, events?, webhook_url, metadata?} | 201 Watch |
GET /v1/watches | — | 200 {watches: Watch[], limits: {max_active_watches}} |
GET /v1/watches/{id} | — | 200 Watch |
POST /v1/watches/{id}/pause and /resume | — | 200 Watch; idempotent |
DELETE /v1/watches/{id} | — | 200 {id, status: "deleted"}; the geometry is deleted at once |
GET /v1/watches/{id}/deliveries | — | 200 {deliveries: Delivery[]}, the last 30 days |
POST /v1/webhook-secret | — | 201 {secret, version, created_at}: creates or rotates the project's signing secret. The secret is shown here only, once |
GET /v1/webhook-secret | — | 200 {version, created_at}, or 404 when none exists. Never the secret |
Request fields:
-
line: a GeoJSON LineString ([lon, lat]), or{gpx: "<gpx document>"}; at most 5,000 points and 120 km, one part (R15's limits). -
fromandto: dates intimezone, at most 7 days apart and starting within 14 days. -
start_times: 1–8 local times. Default:06:00to10:00hourly. -
pace,prefer: passed to R15 as in its contract. -
events: a subset ofwindow_changed,thunderstorm_signal,closure,snow. Default: all. -
webhook_url:https://only, on port 443 or 8443 (since 6 Oct 2026), public hosts only (no IP literals, loopback, private or link-local addresses, nor TrailSplits' own servers), at most 2,048 characters. Checked when the watch is saved and again at every delivery. -
metadata: at most 1 KB of JSON, echoed in every event. Never put personal data in it.
Errors
Errors use the shape {error, status, message, limit?} from api-keys-usage-v1:
| Status | error | When |
|---|---|---|
| 400 | invalid_request | a field fails the rules above; the message names it |
| 403 | scope_denied | a publishable key |
| 404 | not_found | an unknown watch, or one of another project |
| 409 | watch_limit | the plan's active watches are in use (limit: "max_active_watches") |
| 409 | webhook_secret_required | the project has no signing secret yet; POST /v1/webhook-secret first |
| 422 | line_refused | R15 refuses the line (too long, several parts, off any mapped path); the message is R15's |
Evaluation
-
Every 3 hours while the watch is active, or every hour in the last 48 hours before
from. -
Each evaluation calls R15 once with the stored line, and compares the result with
last_state.
Events:
| Event | Fires when |
|---|---|
window_changed | the best window (R15's best.id, <date>T<start> local) is a different one |
thunderstorm_signal | a thunderstorm signal appears in, or clears from, a window that was best |
closure | later: an official closure or correction along the line is added or lifted. R15 has no closures producer yet |
snow | later: the snow state along the line changes class (snow evidence stays in the route brief for now) |
ended | to has passed. The watch stops and its geometry is deleted the next day |
Webhooks
What is sent: POST to webhook_url, with Content-Type: application/json:
{
"id": "evt_…",
"type": "window_changed",
"watch_id": "wch_…",
"occurred_at": "2026-10-03T12:00:04Z",
"data": { "previous": { "date": "2026-10-05", "start": "07:00" }, "current": { "date": "2026-10-06", "start": "06:00" }, "why": ["dry at arrival", "daylight margin 3 h"] },
"metadata": { "trip": "abc" }
}
Headers:
-
X-TrailSplits-Event-Id: evt_…(the same id on every retry: dedupe on it); -
X-TrailSplits-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>.
Receivers should reject a t more than 5 minutes old.
Delivery and retries:
- A 2xx within 10 s is delivered.
-
Otherwise retries follow at 1 min, 5 min, 30 min, 2 h and 6 h. After that the event is
failed. -
20 failed events in a row set the watch to
failing, and it stops evaluating until it is resumed. - Redirects are not followed.
- Per watch, events go out in order.
Deliveries log:
{ "event_id": "evt_…", "type": "window_changed", "at": "…Z", "attempts": 2, "status": "delivered", "response_status": 200 }
status is delivered, retrying or failed. The log keeps no payloads.
Metering
Provisional: the owner sets pricing numbers. These follow the published credit table.
- Each active watch costs 1 credit per day.
-
Both show under the operation
watchesin/v1/usage.
Active watches per project:
| Free | Starter | Pro | Business |
|---|---|---|---|
| 5 | 25 | 100 | 500 |
Privacy and data
-
The route geometry is stored only while the watch exists.
DELETE, or the day afterto, removes it. - It is never shared or used for anything else. The privacy page says so.
- Delivery logs keep event ids, types, times and status codes for 30 days, never bodies.