API Documentation
All Travel Trends data is published as static, versioned JSON under /data/api/v1/. No authentication, no rate limits beyond CDN defaults. Responses are regenerated by the ETL pipeline; check generated_at for freshness.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /data/api/v1/index.json | API root: available collections, schema version, generated_at. |
| GET | /data/api/v1/events.json | Event index: all detected events with id, type, severity, status, countries, title, and report_url. Sorted newest first. |
| GET | /data/api/v1/events/{id}.json | Full event report: detection disclosure, impact magnitudes, sources with snapshots, labeled estimates, and recommendations. |
| GET | /data/api/v1/countries/{code}/exposure.json | Per-country exposure summary (ISO2 code): active events, disruption context, and tourism baseline. |
| GET | /data/api/v1/sources.json | Source catalog: every upstream feed with id, name, URL, access type, and update frequency. |
| GET | /data/api/v1/methodology/backtest.json | Detection-quality backtest: hand-curated historical episodes (each with a public source URL) replayed against the monthly disruption-index detector — precision, recall, and per-episode hit/miss detail. |
| GET | /data/api/v1/status.json | Operational status: per-source health, freshness, coverage matrix and explicit blind spots (domains/countries not monitored live). |
| GET | /data/api/v1/track-record.json | Public track record: every event ever flagged, each with first-detection timestamp, full severity history, what we said (verbatim first brief line), detection rule, sources cited and lifecycle outcome. Append-only by construction — archive_sha256 publishes the real sha256 of the underlying lifecycle archive so the record is verifiable and cannot be edited retroactively. Includes stats, an honest coverage_note while the archive is young, and backtest_ref quoting published precision/recall. |
| GET | /data/api/v1/briefing.json | Daily situation briefing: currently active events with severity summary and links to full reports — the machine-readable counterpart of the homepage briefing hero. |
| GET | /data/api/v1/trips/{id}.json | A watched trip as one JSON file: the persisted monitored object, its append-only updates log (creation record plus one entry per material change, never edited), and a latest-assessment snapshot (decision-impact lines, confidence with coverage/blind-spots, sources checked). Powers the human /trip/{id} view and lets an AI agent read the same continuity payload it gets from the MCP tools. |
| GET | /data/api/v1/methodology/value_margin.json | Decision-support value bar vs a frontier-LLM+web concierge baseline. Two axes: Axis A (one-shot answer quality) and Axis B (continuity — alert only on material change, catch every material change, suppress the noise a stateless concierge re-dumps). Publishes per-case and aggregate margins plus a PASS/PARTIAL/FAIL verdict. All scores are labeled judge/model outputs; a partial result (won continuity, conceded one-shot) is reported honestly via axis_a_lost. |
Example
curl -s https://travel-trends.mmatinca.eu/data/api/v1/events.json \
| jq '.events[] | select(.status == "active") | {id, severity, title: .title.en}'Event report schema
| Field | Meaning |
|---|---|
| schema_version | Version of the report schema; bump indicates breaking changes. |
| id | Stable event identifier (slug). Used in the events/{id}.json path. |
| status | "active" while the detector still observes the condition; "ended" afterwards. |
| type / domain | Detector type (e.g. weather_warning_spike) and risk domain (weather, transit, health...). |
| severity | One of info | watch | warning | severe, assigned by disclosed thresholds. |
| title | i18n key + params, plus a canonical English rendering in title.en. |
| countries | Affected countries as ISO2 codes. |
| onset_date / first_detected_at / last_observed_at / ended_at | Event lifecycle timestamps (ISO 8601). ended_at is null while active. |
| detection | Full disclosure: method, rule text, thresholds, statistical baseline (n_points, window_hours, mean, std, sufficient), and observed values. |
| impact | Observed magnitudes plus disruption-index context for the latest informative month. |
| sources | Every upstream record used: source_id, name, url, snapshot_url, collected_at. |
| estimates | Labeled model outputs: historical_correlation and analogues blocks, each with method, caveats — or null when unavailable. Always read the disclaimer. |
| recommendations | Audience-tagged actions (tmc, hotel, ota, tour_operator) with text_key + params and rendered text. |
| ai_summary | Optional LLM-written narrative with model id and disclaimer; null when not generated. |
| links | api (this JSON), html (the human page), methodology (anchor on this page). |
Methodology
Detectionis fully deterministic: each event type is produced by a rule with disclosed thresholds, published verbatim in the report's detection block. Where a statistical baseline is used, the report discloses the number of points, window, mean, and standard deviation; when the baseline is insufficient, the detector falls back to an absolute threshold and says so.
Backtest: the monthly disruption-index detector is replayed point-in-time over the full published history and scored against a hand-curated set of known European disruption episodes (2014–2026), each backed by a public source URL, plus documented-calm negative months. Precision, recall, and per-episode hit/miss detail — including a deliberately labeled blind-spot miss — are published verbatim with their caveats at /data/api/v1/methodology/backtest.json. The backtest covers the monthly detector only; live detectors have insufficient history to backtest.
Estimates are labeled model outputs — historical correlation against the disruption index and analogue-episode matching — never presented as observations. Each block carries its method name, label, and caveats.
No-fabrication policy: every number in a report is either directly observed from a cited source snapshot or explicitly labeled as a model estimate with its method and caveats; nothing is invented to fill gaps.
Remote MCP — ChatGPT custom connector
A hosted MCP server is exposed over Streamable HTTP at a stable HTTPS endpoint, so it can be added as a custom connector in ChatGPT (and other remote MCP clients) without running anything locally. It exposes a deliberately small surface — 8 tools — that cover the decision-support and continuity workflows. Public, no authentication— just paste the URL and use it; we are still expanding coverage and want adoption.
Exposed tools (8): assess_trip, watch_trip, whats_changed (the change alert; accepts audience for persona actions), explain_silence (the silence audit: what we checked and ruled out), get_trip_updates_since (notification payload since a checkpoint), country_briefing (alias over get_daily_brief), list_events, and country_tourism_profile (a decade of Eurostat tourism per country: seasonality, foreign dependency, recovery vs 2019 — every figure computed from the published dataset). See the tool reference below for inputs and outputs. The remaining stdio-only tools (get_event_report, get_country_exposure, find_analogues, query_disruption_index, list_sources, get_trip_updates) stay private to keep the remote surface focused for LLM tool selection — the underlying data is still on /data/api/v1/ for direct HTTP access.
Add it to ChatGPT:
- Open ChatGPT → Settings → Connectors → Add custom connector.
- Pick “MCP” (Streamable HTTP) as connector type.
- Paste the endpoint URL above.
- Auth: None — leave authentication empty / pick “No auth”.
- Save. ChatGPT calls
tools/listand the 8 tools become available in chats.
Pilot: webhook alerts for your trip portfolio. watch_trip accepts a notify_webhook_url— on every material change (including strikes announced before your travellers depart) we POST the signed update to your endpoint. If you run trips for others (TMC, tour operator, OTA) and want to pilot this on a real portfolio, write us — email available on request. No payment, no lock-in; we want honest feedback from real operations.
Feedback or coverage requests — email available on request. Discovery is reachable from a browser or curl— useful to confirm the connector sees the same metadata ChatGPT will:
# 1. Discovery — confirms server identity + tool list
curl -s https://travel-trends.mmatinca.eu/.well-known/mcp.json | jq
curl -s https://travel-trends.mmatinca.eu/mcp/info | jq .status
# 2. MCP handshake (Streamable HTTP — captures session id)
curl -sS -D - -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05",
"capabilities":{},
"clientInfo":{"name":"curl","version":"1"}}}' \
https://travel-trends.mmatinca.eu/mcp/v1
# 3. Call assess_trip (Mcp-Session-Id header from step 2)
curl -sS -X POST \
-H "Mcp-Session-Id: <session-id-from-step-2>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"assess_trip",
"arguments":{"destinations":["RO","HR"],
"date_from":"2026-07-01",
"date_to":"2026-07-15"}}}' \
https://travel-trends.mmatinca.eu/mcp/v1The hosted MCP is read-only against the published artifacts; it inherits the same no-fabrication policy as the rest of the API. Rate-limited at the edge. A 502 means the upstream MCP process is being restarted (check /mcp/info).
Local MCP — Claude Code (stdio)
The ETL repository ships a stdio MCP server exposing the same data as tools — both read-only data tools (list events, fetch report, country exposure, daily brief, sources, disruption index, analogues) and a decision-support trio that answers “should I care about THIS trip?” rather than returning raw data:
assess_trip— given destinations and a travel window, returns a travel status (NORMAL/MINOR/MAJOR_DISRUPTION), per-event decision-impactaction lines, a labeled confidence score with its caveats, and the sources checked. Destinations are the EU-27 ISO2 codes (Greece = “EL”) plus the non-EU27 countries we actively monitor: Norway (“NO”, rail via Entur), the United Kingdom (“UK”/“GB”, transit via TfL), and Switzerland (“CH”, rail via SBB — key-pending, so it is reported as a declared blind spot until the feed is keyed, never a false all-clear). A country we do not monitor is rejected with an explicitunknown_countryerror rather than a fabricated all-clear. Destinations also accept natural input — IATA airport codes (e.g.TSR,AMS) and city names (e.g. “Timișoara”, “Amsterdam”) — resolved deterministically to a monitored country code, with aresolvedlist in the response disclosing how each token was mapped (e.g. TSR → RO via iata-airport). Resolution adds no coverage: it fixes input only, and a token we cannot place unambiguously is rejected withunknown_destinationrather than guessed. Sub-floor noise (e.g. a deep far-field seismic blip) is omitted; calm is a monitoring result, never an invented forecast.Passlang(e.g.de,ro,pl,fr,es,it; default English) to answer in the traveller's language — the highest-value case is a foreign traveller in a country whose language they do not speak. The response gains alocalizedblock with the status sentence, an honest reassurance line (calm only when the status is NORMAL), the decision-impact lines, and — never dropped or softened — the localizedcaveatsandblind_spots. Our own wording is translated from deterministic on-disk templates; where a language has no template it falls back to English and flags it inlocalized.fallback_lang_parts— never a fabricated translation. The only machine-translated text is the source-derived free text the traveller genuinely cannot read (an event headline in the source language), rendered via Gemini and always carried with the label “AI-translated — verify against the linked official source”; when no API key is set or a translation fails, the original source text is kept with an honest note, never a fake translation. Localization never becomes a false all-clear.When aviation is a genuine blind spot for the destination, the answer adds anaviation_handoff— a signpost, not coverage. It explicitly discloses that we are NOT a live source for your flight and hands you off to the authoritative ones: your airline app and the airport operator's own official live-status page (from a curated public links seed). It discloses the gap; it never masks it or implies we watch your flight.watch_trip— the continuity primitive: persists the trip as a monitored object (idempotent on identity) so later runs append an update only when something materially changes — what a stateless chat session cannot natively do.get_trip_updates— returns a watched trip's timeline (trip memory): the creation record plus one entry per material change. Trip ids are regex-validated before any path is built.whats_changed— the continuity delta a memoryless chat cannot produce: recomputes the trip's current assessment and returns only what movedsince a given checkpoint (announce → confirmed → cleared, travel-status and confidence shifts) as a plain-language line. A non-material re-check returnsmaterial=falsewith a flagged “no material change” — never invented churn.explain_silence— the silence audit for a calm watched trip: answers “why didn't I hear from you?” with the re-check count, what was suppressedbelow the disclosed relevance floor (e.g. a deep quake at modelled felt-intensity 3.39 < floor 4.0) and the named blind spots — domains we do not monitor live, so silence there is not a guarantee.get_trip_updates_since— the notification payload: the updates log filtered to entries strictly after asincetimestamp, so a consumer can push only what is new since the user last looked.
Register it with Claude Code from the repository root:
claude mcp add travel-trends -- .venv/bin/python -m etl.mcp_serverBy default the server reads artifacts from the local public/data build output. Set the DATA_BASE environment variable to point it at another artifact directory or a deployed base URL (e.g. DATA_BASE=https://travel-trends.mmatinca.eu/data) so the tools serve the same JSON as this site.