This document defines the backend API needed by the Flutter frontend for direct navigation and AI-assisted trip planning.
Base URL is provided to Flutter as QUESTMAP_API_BASE_URL and defaults to
https://back.hack5.yandrik.dev. Request and response JSON owned by Questmap
uses camelCase field names. Existing upstream proxy endpoints keep Valhalla and
MOTIS field names where noted.
- All endpoints accept and return JSON unless the endpoint is explicitly an SSE stream.
- Coordinates use WGS84 decimal degrees:
{ "lat": 48.401, "lon": 9.99 }. - Date/time values are ISO 8601 strings with timezone whenever available.
- Unknown optional fields should be ignored by clients and preserved where practical by the backend.
- Validation errors return
422with a FastAPI/Pydantic-style detail body. - Upstream routing/transit failures return
502unless the upstream error is a client/actionable error forwarded as described below.
Returns backend health.
{ "status": "ok" }Returns database health.
{
"status": "ok",
"url": "ws://localhost:8001",
"namespace": "questmap",
"database": "questmap"
}Checks Valhalla availability.
{
"status": "ok",
"url": "http://localhost:8002",
"upstream": {}
}Returns 503 when Valhalla is unavailable.
Checks MOTIS availability.
{
"status": "ok",
"url": "http://localhost:8010",
"upstream": {}
}Returns 503 when MOTIS is unavailable.
The frontend asks for direct navigation after the user selects a target and a
transport mode. It normalizes returned Valhalla/MOTIS payloads into
NavigationCandidate locally, so v1 backend responses for these endpoints must
remain compatible with those upstream response shapes.
Routes walk, bike, and drive requests through Valhalla /route.
Request body uses Valhalla field names. Flutter sends:
{
"locations": [
{ "lat": 48.401, "lon": 9.99, "type": "break", "name": "Start" },
{ "lat": 48.42, "lon": 10.01, "type": "break", "name": "Destination" }
],
"costing": "bicycle",
"alternates": 3,
"directions_type": "none",
"shape_format": "geojson",
"units": "kilometers"
}Required fields:
locations: at least two Valhalla locations. V1 product flow sends exactly two points.costing: one ofpedestrian,bicycle,autofor app direct navigation.
Important behavior:
- Forward the request to Valhalla.
- Keep
shape_format=geojsonsupport because Flutter reads either GeoJSON line strings or encoded polylines. - Honor
alternatesfor two-point requests. Valhalla forces alternatives to zero for routes with more than two waypoints, but the v1 frontend only sends two. - Do not generate turn-by-turn directions for v1; the app sends
directions_type=none.
Response body is the Valhalla route response, including:
{
"trip": {
"summary": { "length": 2.4, "time": 720 },
"shape": {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[9.99, 48.401], [10.01, 48.42]]
}
},
"legs": []
},
"alternates": [
{
"trip": {
"summary": { "length": 2.8, "time": 780 },
"legs": []
}
}
]
}Error behavior:
- Forward upstream
400and429as the same status with upstream text indetail. - Return
502for other Valhalla failures.
Plans public transport navigation through MOTIS /api/v6/plan.
Request body uses MOTIS field names in JSON. Flutter sends:
{
"fromPlace": "48.401,9.99",
"toPlace": "48.42,10.01",
"time": "2026-06-07T12:00:00.000Z",
"detailedLegs": true,
"detailedTransfers": true,
"directModes": [],
"preTransitModes": ["WALK"],
"postTransitModes": ["WALK"],
"transitModes": ["TRANSIT"],
"numItineraries": 4,
"numLegAlternatives": 3,
"timetableView": false,
"language": ["de", "en"]
}Required fields:
fromPlace:"lat,lon".toPlace:"lat,lon".
Important behavior:
- Forward the request to MOTIS as
/api/v6/planquery parameters. - Lists are serialized as comma-separated query values for MOTIS.
- Return detailed legs and detailed transfers when requested.
- Treat returned MOTIS itineraries as selectable route alternatives.
Response body is the MOTIS plan response. Flutter expects at least:
{
"itineraries": [
{
"id": "itinerary-1",
"duration": 1800,
"legs": [
{
"mode": "WALK",
"from": { "name": "Start" },
"to": { "name": "Stop A" },
"duration": 300,
"distance": 420,
"legGeometry": { "points": "encoded-polyline", "precision": 6 }
},
{
"mode": "TRAM",
"displayName": "U2",
"routeShortName": "U2",
"from": { "name": "Stop A" },
"to": { "name": "Stop B" },
"duration": 900,
"legGeometry": { "points": "encoded-polyline", "precision": 6 }
}
]
}
]
}Error behavior:
- Forward upstream
400,404,422, and429as the same status with upstream text indetail. - Return
502for other MOTIS failures.
Trip planning is a backend-owned AI workflow. The Flutter app sends a rough, editable itinerary draft and then listens to an SSE event stream. The backend may ask questions, emit partial plans, emit a final plan, and support resume after an app restart.
{
"lat": 48.401,
"lon": 9.99,
"label": "Ulm Hbf"
}label is optional.
String enum:
walkbikedrivepublicTransport
Display label for publicTransport in the app is ÖPNV.
{
"startTime": "2026-06-07T12:00:00.000Z",
"arrivalTime": "2026-06-07T14:00:00.000Z",
"durationMinutes": 60
}Rules:
durationMinutesis required and must be positive.startTimeis optional.arrivalTimeis optional.- If both
startTimeandarrivalTimeare present, the backend should validate that the interval can fitdurationMinutes.
Exact selected point:
{
"type": "exactPoint",
"point": { "lat": 48.401, "lon": 9.99, "label": "Museum" }
}Around a selected point:
{
"type": "aroundPoint",
"point": { "lat": 48.401, "lon": 9.99, "label": "City center" }
}Somewhere in a circular area:
{
"type": "areaCircle",
"center": { "lat": 48.401, "lon": 9.99 },
"radiusMeters": 800
}Backend chooses anywhere within a transport budget:
{
"type": "wherever",
"maxTransportMinutes": 15
}{
"id": "step-1",
"type": "eat",
"title": "Eat",
"details": "Chinese, casual dinner",
"time": { "durationMinutes": 60 },
"location": {
"type": "wherever",
"maxTransportMinutes": 15
},
"iconKey": "restaurant",
"colorValue": 4293869636
}type enum:
shopeatpartywalksightseemeanderexactLocation
iconKey and colorValue are UI hints sent by the app; the backend can ignore
them. For meander, the backend may combine shopping, food, views, walking, and
nightlife based on context.
{
"id": "plan-1",
"title": "Afternoon in Ulm",
"summary": "Food, a riverside walk, and one viewpoint.",
"items": [
{
"id": "travel-1",
"type": "travel",
"title": "Walk to the restaurant",
"description": "A short walk through the old town.",
"reasoning": "Walking is faster than waiting for transit here.",
"transportMode": "walk",
"startTime": "2026-06-07T12:00:00.000Z",
"endTime": "2026-06-07T12:12:00.000Z",
"geometry": [
{ "lat": 48.401, "lon": 9.99 },
{ "lat": 48.404, "lon": 9.995 }
]
},
{
"id": "activity-1",
"type": "activity",
"title": "Lunch",
"description": "Chinese lunch near the center.",
"reasoning": "Matches the requested cuisine and timing.",
"sourceDraftStepId": "step-1",
"stepType": "eat",
"startTime": "2026-06-07T12:15:00.000Z",
"endTime": "2026-06-07T13:15:00.000Z",
"location": { "lat": 48.404, "lon": 9.995, "label": "Restaurant" },
"geometry": []
}
]
}Rules:
itemsare ordered and represent the full route chain: travel leg, activity, travel leg, next activity, and so on.typeisactivityortravel.descriptionshould be visible by default in the app.reasoningis optional and shown on tap.- Activity items should include
sourceDraftStepIdwhen derived from a draft step. - Travel items should include
transportModeand routegeometrywhen available. - Activity items should include
locationwhen the backend chose or confirmed a point.
Starts an AI trip-planning session.
Request:
{
"draftId": "draft-1",
"startLocation": { "lat": 48.401, "lon": 9.99, "label": "Current location" },
"endLocation": { "lat": 48.42, "lon": 10.01, "label": "Hotel" },
"transportModes": ["walk", "publicTransport"],
"steps": [
{
"id": "step-1",
"type": "eat",
"title": "Eat",
"details": "Chinese",
"time": { "durationMinutes": 60 },
"location": { "type": "wherever", "maxTransportMinutes": 15 }
}
]
}Required fields:
draftIdstartLocationtransportModes, at least onesteps, at least one for useful planning
Optional fields:
endLocation
Response:
{ "sessionId": "session-1" }Behavior:
- Create a durable session record before returning.
- Start or enqueue the planning workflow.
- The session must be resumable through
GET /trip-planning/sessions/{sessionId}. - The backend owns route computation inside generated trip plans, including public transport legs.
Streams live planning events using Server-Sent Events.
Headers:
- Client sends
Accept: text/event-stream. - Server responds with
Content-Type: text/event-stream.
Each event payload is JSON. The SSE event: name may match the payload type,
but Flutter will use data.type when present.
Status event:
event: status
data: {"type":"status","message":"Finding places near your route..."}
Question event:
event: question
data: {"type":"question","question":{"id":"q1","kind":"yesNo","prompt":"Is a 12 minute walk okay?","options":[]}}
Partial plan event:
event: partialPlan
data: {"type":"partialPlan","plan":{"id":"plan-draft","title":"Draft plan","items":[]}}
Final plan event:
event: finalPlan
data: {"type":"finalPlan","plan":{"id":"plan-1","title":"Final plan","items":[]}}
Error event:
event: error
data: {"type":"error","message":"No reachable public transport route was found."}
Done event:
event: done
data: {"type":"done","message":"done"}
Event types:
status: progress message only.question: requires a user answer before the workflow continues.partialPlan: read-only plan progress.finalPlan: final generated plan; the app persists it locally.error: terminal or recoverable error. If terminal, follow withdone.done: stream is complete.
Keepalive comments are allowed:
: keepalive
Question schema:
{
"id": "q1",
"kind": "selection",
"prompt": "Which restaurant direction do you prefer?",
"unit": "minutes",
"options": [
{
"id": "north",
"title": "North route",
"description": "More shops, slightly longer.",
"imageUrl": "https://example.test/preview.jpg",
"payload": { "routeCandidateId": "route-1" }
}
]
}kind enum:
yesNonumbertextselectionrouteChoice
Question value expectations:
yesNo: boolean.number: number.text: string.selection: option id string, or an object if the backend explicitly defines a richer option payload.routeChoice: selected route/option id string, or a route-choice object if specified in the question payload.
Posts an answer to the current agent question.
Request:
{
"questionId": "q1",
"value": true
}Response:
204 No Contenton success.
Behavior:
- Reject answers for unknown sessions with
404. - Reject stale or mismatched
questionIdwith409. - Validate answer type against the question
kind; return422when invalid. - After accepting an answer, continue the workflow and emit further SSE events.
Cancels a running session.
Response:
204 No Contenton success.
Behavior:
- Mark the session as cancelled.
- Stop active work where possible.
- Existing event streams should receive either an
errorevent with a cancellation message followed bydone, or justdone. - Repeated cancellation is idempotent and should still return
204. - Unknown sessions return
404.
Returns a session snapshot for app restart/resume.
Response:
{
"sessionId": "session-1",
"draftId": "draft-1",
"state": "waitingForAnswer",
"request": {
"draftId": "draft-1",
"startLocation": { "lat": 48.401, "lon": 9.99 },
"transportModes": ["walk", "publicTransport"],
"steps": []
},
"currentQuestion": {
"id": "q1",
"kind": "yesNo",
"prompt": "Is a 12 minute walk okay?",
"options": []
},
"latestPartialPlan": null,
"finalPlan": null,
"lastMessage": "Waiting for your answer.",
"createdAt": "2026-06-07T12:00:00.000Z",
"updatedAt": "2026-06-07T12:02:00.000Z"
}state enum:
queuedrunningwaitingForAnswercompletedfailedcancelled
Rules:
- Return
404for unknown sessions. - Include
currentQuestiononly when waiting for an answer. - Include
latestPartialPlanwhen available. - Include
finalPlanwhen completed. - A client may reconnect to
/eventsafter reading this snapshot.
The Flutter app persists drafts, final plans, active agent sessions, and trip progress locally. The backend still needs durable trip-planning session storage for:
- returning
sessionIdonly after a session can be resumed, - reconnecting an SSE stream,
- validating posted answers,
- cancellation,
- exposing session snapshots.
Backend storage of final user plan history and multi-device sync are out of v1.
- Turn-by-turn instructions and lane guidance.
- Automatic rerouting during active navigation.
- Payment, ticketing, or fare purchase.
- Backend-owned saved plan history beyond active/resumable planning sessions.
- Multi-device sync.