{"openapi":"3.1.0","info":{"title":"RideTogether API","version":"1.0.0","summary":"Backend for organising shared sporting outings.","description":"REST API for RideTogether: shared cycling, hiking, running and cross-country skiing outings\nwith a planned GPX route, live participant positions and shared expenses.\n\n## Conventions\n\n- **Time**: every timestamp is ISO 8601 in **UTC** with a `Z` suffix. The server stores\n  `timestamptz`; clients render in the device’s local zone. Requests may send any offset.\n- **Money**: every monetary amount is an **integer in minor currency units** (haléře for CZK).\n  450,50 Kč is `45050`. No floating point value ever touches the money path.\n- **Coordinates**: GeoJSON order, `[longitude, latitude]`, optionally followed by elevation in\n  metres. Never `[latitude, longitude]`.\n- **Pagination**: opaque cursors. Pass `nextCursor` back as `cursor`; treat it as a black box.\n- **Errors**: every failure returns the same envelope — see the `ApiError` schema. Switch on\n  `error.code`, not on `error.message`.\n- **Idempotency**: `POST /locations/batch` is idempotent on `(sessionId, sequenceNumber)`, and\n  `POST /join/{joinCode}` is idempotent per user.\n\n## Authentication\n\nSend `Authorization: Bearer <accessToken>`. Access tokens are short-lived JWTs; refresh tokens\nare opaque, single-use and stored server-side only as a hash. Rotate through\n`POST /api/v1/auth/refresh`. Presenting an already-used refresh token revokes the whole\nrotation family and returns `REFRESH_TOKEN_REUSED` — the correct client response is to sign in\nagain.\n\n## Privacy\n\nLocation is sensitive. A user shares their position only while THEY have an active tracking\nsession; nobody can start tracking on someone else’s device. Trip data is visible to members\nonly, and non-members receive `404` rather than `403` so trip ids cannot be probed.\n\n## Realtime\n\nPolling `GET /trips/{tripId}/locations/latest` is the guaranteed baseline and never depends on a\npersistent connection. A push transport may be added later without changing any payload shape.","contact":{"name":"RideTogether"},"license":{"name":"Proprietary","identifier":"LicenseRef-Proprietary"}},"servers":[{"url":"https://leteckaliga.cz","description":"This deployment"},{"url":"http://localhost:3000","description":"Local development"}],"tags":[{"name":"Authentication","description":"Registration, sign-in, token rotation, device sessions."},{"name":"Users","description":"Profiles and avatars."},{"name":"Trips","description":"Creating trips and moving them through their lifecycle."},{"name":"Membership","description":"Invitations, join codes, roles."},{"name":"Routes","description":"GPX import and the planned route."},{"name":"Tracking","description":"Tracking sessions, GPS batches, live positions, recorded tracks."},{"name":"Chat","description":"A trip’s conversation. Polled; ordered by the server’s clock."},{"name":"SOS","description":"Calling for help on a trip, and answering somebody who has. Polled faster than anything else here."},{"name":"Social","description":"Friends and recent fellow participants."},{"name":"Expenses","description":"Shared expenses, settlements, Czech QR payments."},{"name":"System","description":"Health and diagnostics."}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Short-lived access token from `/api/v1/auth/login` or `/api/v1/auth/refresh`."}},"schemas":{"ApiError":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/ApiErrorDetail"}},"required":["error"]},"ApiErrorDetail":{"type":"object","properties":{"code":{"type":"string","description":"Stable machine-readable error code."},"message":{"type":"string","description":"Human-readable summary. May change without notice."},"details":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}],"description":"Error-specific payload. For VALIDATION_ERROR: { fields: { path: string[] } }."},"requestId":{"type":"string","description":"Correlates the response with server logs."}},"required":["code","message","details","requestId"]},"ActivityType":{"type":"string","enum":["CYCLING","MOUNTAIN_BIKING","HIKING","RUNNING","CROSS_COUNTRY_SKIING","OTHER"]},"TripStatus":{"type":"string","enum":["DRAFT","PLANNED","ACTIVE","FINISHED","CANCELLED"]},"TripRole":{"type":"string","enum":["OWNER","ORGANIZER","MEMBER"]},"TripMemberStatus":{"type":"string","enum":["INVITED","ACTIVE","LEFT","REMOVED","DECLINED"]},"InvitationStatus":{"type":"string","enum":["PENDING","ACCEPTED","DECLINED","REVOKED","EXPIRED"]},"DevicePlatform":{"type":"string","enum":["IOS","ANDROID","WEB"]},"TrackingSessionStatus":{"type":"string","enum":["ACTIVE","PAUSED","FINISHED","ABORTED"]},"FriendshipStatus":{"type":"string","enum":["PENDING","ACCEPTED","DECLINED","BLOCKED"]},"SplitMethod":{"type":"string","enum":["EQUAL","CUSTOM","SHARES"]},"SettlementStatus":{"type":"string","enum":["OPEN","PARTIALLY_PAID","PAID","WAIVED"]},"LocationFreshness":{"type":"string","enum":["LIVE","DELAYED","STALE","OFFLINE"]},"RouteSource":{"type":"string","enum":["GPX_UPLOAD"]},"Position":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}],"items":{"type":"number"},"description":"[longitude, latitude] with optional elevation in metres. Never [lat, lon]."},"LineString":{"type":"object","properties":{"type":{"type":"string","const":"LineString"},"coordinates":{"type":"array","items":{"$ref":"#/components/schemas/Position"}}},"required":["type","coordinates"]},"MultiLineString":{"type":"object","properties":{"type":{"type":"string","const":"MultiLineString"},"coordinates":{"type":"array","items":{"type":"array","items":{"$ref":"#/components/schemas/Position"}}}},"required":["type","coordinates"]},"RouteGeometry":{"oneOf":[{"$ref":"#/components/schemas/LineString"},{"$ref":"#/components/schemas/MultiLineString"}]},"BoundingBox":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"},{"type":"number"},{"type":"number"}],"description":"[west, south, east, north]"},"PublicUser":{"type":"object","properties":{"id":{"type":"string"},"displayName":{"type":"string"},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Short-lived signed URL, or null when no avatar is set."}},"required":["id","displayName","avatarUrl"]},"PrivateUser":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"avatarStorageKey":{"anyOf":[{"type":"string"},{"type":"null"}]},"bankAccount":{"$ref":"#/components/schemas/BankAccount"},"currency":{"type":"string"},"locale":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["id","email","displayName","avatarUrl","avatarStorageKey","bankAccount","currency","locale","createdAt","updatedAt"]},"BankAccount":{"type":"object","properties":{"accountPrefix":{"anyOf":[{"type":"string"},{"type":"null"}]},"accountNumber":{"anyOf":[{"type":"string"},{"type":"null"}]},"bankCode":{"anyOf":[{"type":"string"},{"type":"null"}]},"iban":{"anyOf":[{"type":"string"},{"type":"null"}]},"payable":{"type":"boolean"}},"required":["accountPrefix","accountNumber","bankCode","iban","payable"]},"Device":{"type":"object","properties":{"id":{"type":"string"},"platform":{"$ref":"#/components/schemas/DevicePlatform"},"deviceName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastSeenAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["id","platform","deviceName","lastSeenAt","createdAt"]},"MeResponse":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/PrivateUser"},"devices":{"type":"array","items":{"$ref":"#/components/schemas/Device"}}},"required":["user","devices"]},"UpdateMeRequest":{"type":"object","properties":{"displayName":{"type":"string","minLength":1,"maxLength":120},"locale":{"type":"string","minLength":2,"maxLength":16,"pattern":"^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$"},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"bankAccountPrefix":{"anyOf":[{"type":"string","pattern":"^\\d{0,6}$"},{"type":"null"}]},"bankAccountNumber":{"anyOf":[{"type":"string","pattern":"^\\d{1,10}$"},{"type":"null"}]},"bankCode":{"anyOf":[{"type":"string","pattern":"^\\d{4}$"},{"type":"null"}]},"iban":{"anyOf":[{"type":"string"},{"type":"null"}]}}},"AvatarUploadResponse":{"type":"object","properties":{"avatarStorageKey":{"type":"string"},"avatarUrl":{"type":"string"},"sizeBytes":{"type":"integer","minimum":0,"maximum":9007199254740991},"contentType":{"type":"string"}},"required":["avatarStorageKey","avatarUrl","sizeBytes","contentType"]},"PublicUserProfile":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/PublicUser"},"finishedTripCount":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["user","finishedTripCount"]},"DeviceInput":{"type":"object","properties":{"platform":{"$ref":"#/components/schemas/DevicePlatform"},"deviceName":{"description":"Human label, e.g. \"Petr's iPhone 15\".","anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"pushToken":{"description":"APNs / FCM token. Optional.","anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"deviceId":{"description":"Reuse a previously registered device instead of creating one.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]}},"required":["platform"]},"RefreshDeviceInput":{"type":"object","properties":{"platform":{"$ref":"#/components/schemas/DevicePlatform"},"deviceName":{"description":"Human label, e.g. \"Petr's iPhone 15\".","anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"pushToken":{"description":"APNs / FCM token. Optional.","anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"deviceId":{"description":"Reuse a previously registered device instead of creating one.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]}}},"RegisterRequest":{"type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"password":{"type":"string","minLength":10,"maxLength":200},"displayName":{"type":"string","minLength":1,"maxLength":120},"locale":{"type":"string","minLength":2,"maxLength":16,"pattern":"^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$"},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"device":{"$ref":"#/components/schemas/DeviceInput"}},"required":["email","password","displayName","device"]},"LoginRequest":{"type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"password":{"type":"string","minLength":1,"maxLength":200},"device":{"$ref":"#/components/schemas/DeviceInput"}},"required":["email","password","device"]},"RefreshRequest":{"type":"object","properties":{"refreshToken":{"type":"string","minLength":20,"maxLength":500},"device":{"$ref":"#/components/schemas/RefreshDeviceInput"}},"required":["refreshToken"]},"LogoutRequest":{"type":"object","properties":{"refreshToken":{"type":"string","minLength":20,"maxLength":500},"deviceId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}},"LogoutResponse":{"type":"object","properties":{"revokedTokens":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["revokedTokens"]},"TokenPair":{"type":"object","properties":{"accessToken":{"type":"string"},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"accessTokenExpiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"refreshToken":{"type":"string","description":"Opaque. Store in the Keychain / Keystore, never in plain preferences."},"refreshTokenExpiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"tokenType":{"type":"string","const":"Bearer"},"deviceId":{"type":"string"}},"required":["accessToken","expiresIn","accessTokenExpiresAt","refreshToken","refreshTokenExpiresAt","tokenType","deviceId"]},"AuthSession":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/PrivateUser"},"tokens":{"$ref":"#/components/schemas/TokenPair"}},"required":["user","tokens"]},"CreateTripRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":160},"description":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}]},"notes":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}]},"activityType":{"$ref":"#/components/schemas/ActivityType"},"startsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]},"endsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]}},"required":["name","activityType"]},"UpdateTripRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":160},"description":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}]},"notes":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}]},"activityType":{"$ref":"#/components/schemas/ActivityType"},"startsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]},"endsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]}}},"TripSummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"activityType":{"$ref":"#/components/schemas/ActivityType"},"status":{"$ref":"#/components/schemas/TripStatus"},"startsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"endsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"owner":{"$ref":"#/components/schemas/PublicUser"},"myRole":{"$ref":"#/components/schemas/TripRole"},"myStatus":{"$ref":"#/components/schemas/TripMemberStatus"},"activeMemberCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"hasRoute":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["id","name","description","activityType","status","startsAt","endsAt","owner","myRole","myStatus","activeMemberCount","hasRoute","createdAt","updatedAt"]},"TripDetail":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"activityType":{"$ref":"#/components/schemas/ActivityType"},"status":{"$ref":"#/components/schemas/TripStatus"},"startsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"endsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"owner":{"$ref":"#/components/schemas/PublicUser"},"myRole":{"$ref":"#/components/schemas/TripRole"},"myStatus":{"$ref":"#/components/schemas/TripMemberStatus"},"activeMemberCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"hasRoute":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Organiser notes. Visible to all members."},"joinCode":{"anyOf":[{"type":"string"},{"type":"null"}]},"joinUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Universal link encoded into the invitation QR code."},"joinCodeExpiresAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"members":{"type":"array","items":{"$ref":"#/components/schemas/TripMember"}}},"required":["id","name","description","activityType","status","startsAt","endsAt","owner","myRole","myStatus","activeMemberCount","hasRoute","createdAt","updatedAt","notes","joinCode","joinUrl","joinCodeExpiresAt","members"]},"TripPreview":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"activityType":{"$ref":"#/components/schemas/ActivityType"},"status":{"$ref":"#/components/schemas/TripStatus"},"startsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"endsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"owner":{"$ref":"#/components/schemas/PublicUser"},"activeMemberCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"hasRoute":{"type":"boolean"},"alreadyMember":{"type":"boolean"}},"required":["id","name","description","activityType","status","startsAt","endsAt","owner","activeMemberCount","hasRoute","alreadyMember"]},"TripMember":{"type":"object","properties":{"userId":{"type":"string"},"user":{"$ref":"#/components/schemas/PublicUser"},"role":{"$ref":"#/components/schemas/TripRole"},"status":{"$ref":"#/components/schemas/TripMemberStatus"},"joinedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"leftAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]}},"required":["userId","user","role","status","joinedAt","leftAt"]},"TripSummaryPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TripSummary"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."}},"required":["items","nextCursor"]},"TripMemberPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TripMember"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."}},"required":["items","nextCursor"]},"CreateInvitationRequest":{"type":"object","properties":{"userId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"role":{"default":"MEMBER","type":"string","enum":["ORGANIZER","MEMBER"]},"expiresInHours":{"default":336,"type":"integer","minimum":1,"maximum":720}}},"Invitation":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"invitedByUserId":{"type":"string"},"invitedUserId":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"$ref":"#/components/schemas/InvitationStatus"},"expiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"respondedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"inviteUrl":{"type":"string"}},"required":["id","tripId","invitedByUserId","invitedUserId","email","status","expiresAt","createdAt","respondedAt"]},"InvitationPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Invitation"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."}},"required":["items","nextCursor"]},"UpdateMemberRequest":{"type":"object","properties":{"role":{"type":"string","enum":["ORGANIZER","MEMBER"]}}},"JoinTripRequest":{"default":{},"type":"object","properties":{"invitationToken":{"type":"string","minLength":20,"maxLength":500}}},"TripRoute":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"source":{"$ref":"#/components/schemas/RouteSource"},"originalFilename":{"anyOf":[{"type":"string"},{"type":"null"}]},"geometry":{"$ref":"#/components/schemas/RouteGeometry"},"simplifiedGeometry":{"$ref":"#/components/schemas/RouteGeometry"},"distanceM":{"type":"integer","minimum":0,"maximum":9007199254740991},"ascentM":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"descentM":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"pointCount":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Number of points in the full geometry."},"simplifiedPointCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"bounds":{"$ref":"#/components/schemas/BoundingBox"},"hasOriginalGpx":{"type":"boolean"},"createdByUserId":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["id","tripId","name","source","originalFilename","geometry","simplifiedGeometry","distanceM","ascentM","descentM","pointCount","simplifiedPointCount","bounds","hasOriginalGpx","createdByUserId","createdAt","updatedAt"]},"RouteUploadResponse":{"type":"object","properties":{"route":{"$ref":"#/components/schemas/TripRoute"},"import":{"$ref":"#/components/schemas/GpxImportSummary"}},"required":["route","import"]},"GpxImportSummary":{"type":"object","properties":{"segmentCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"rawPointCount":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Points found in the file."},"duplicatePointsRemoved":{"type":"integer","minimum":0,"maximum":9007199254740991},"invalidPointsSkipped":{"type":"integer","minimum":0,"maximum":9007199254740991},"waypointCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"simplificationToleranceM":{"type":"number","minimum":0},"trackName":{"anyOf":[{"type":"string"},{"type":"null"}]},"creator":{"anyOf":[{"type":"string"},{"type":"null"}]},"hasElevation":{"type":"boolean"},"durationMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["segmentCount","rawPointCount","duplicatePointsRemoved","invalidPointsSkipped","waypointCount","simplificationToleranceM","trackName","creator","hasElevation","durationMs"]},"CreateTrackingSessionRequest":{"type":"object","properties":{"deviceId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"startedAt":{"description":"Defaults to the server time of the request.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]}}},"TrackingSession":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"userId":{"type":"string"},"deviceId":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"$ref":"#/components/schemas/TrackingSessionStatus"},"startedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"finishedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"lastSequenceNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"pointCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["id","tripId","userId","deviceId","status","startedAt","finishedAt","lastSequenceNumber","pointCount","createdAt","updatedAt"]},"LocationPointInput":{"type":"object","properties":{"sequenceNumber":{"type":"integer","minimum":0,"maximum":2147483647},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"altitude":{"anyOf":[{"type":"number","minimum":-1000,"maximum":20000},{"type":"null"}]},"horizontalAccuracy":{"anyOf":[{"type":"number","minimum":0,"maximum":100000},{"type":"null"}]},"verticalAccuracy":{"anyOf":[{"type":"number","minimum":0,"maximum":100000},{"type":"null"}]},"speed":{"anyOf":[{"type":"number","minimum":0,"maximum":200},{"type":"null"}]},"heading":{"anyOf":[{"type":"number","minimum":0,"maximum":360},{"type":"null"}]},"batteryLevel":{"anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"recordedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},"required":["sequenceNumber","latitude","longitude","recordedAt"]},"LocationBatchRequest":{"type":"object","properties":{"sessionId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"points":{"minItems":1,"maxItems":500,"type":"array","items":{"$ref":"#/components/schemas/LocationPointInput"}}},"required":["sessionId","points"]},"LocationBatchResponse":{"type":"object","properties":{"sessionId":{"type":"string"},"accepted":{"type":"integer","minimum":0,"maximum":9007199254740991},"duplicates":{"type":"integer","minimum":0,"maximum":9007199254740991},"rejected":{"type":"array","items":{"$ref":"#/components/schemas/RejectedPoint"}},"lastAcknowledgedSequenceNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"serverTime":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["sessionId","accepted","duplicates","rejected","lastAcknowledgedSequenceNumber","serverTime"]},"RejectedPoint":{"type":"object","properties":{"sequenceNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"reason":{"type":"string","enum":["DUPLICATE","TIMESTAMP_TOO_FAR_IN_FUTURE","TIMESTAMP_TOO_OLD","TIMESTAMP_BEFORE_SESSION_START","INVALID_COORDINATES"]},"message":{"type":"string"}},"required":["sequenceNumber","reason","message"]},"LatestLocation":{"type":"object","properties":{"userId":{"type":"string"},"user":{"$ref":"#/components/schemas/PublicUser"},"sessionId":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"altitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"horizontalAccuracy":{"anyOf":[{"type":"number"},{"type":"null"}]},"speed":{"anyOf":[{"type":"number"},{"type":"null"}]},"heading":{"anyOf":[{"type":"number"},{"type":"null"}]},"batteryLevel":{"anyOf":[{"type":"number"},{"type":"null"}]},"recordedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"ageSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"freshness":{"$ref":"#/components/schemas/LocationFreshness"}},"required":["userId","user","sessionId","latitude","longitude","altitude","horizontalAccuracy","speed","heading","batteryLevel","recordedAt","ageSeconds","freshness"]},"LatestLocationsResponse":{"type":"object","properties":{"tripId":{"type":"string"},"serverTime":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"transport":{"type":"string","const":"polling"},"pollIntervalSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"locations":{"type":"array","items":{"$ref":"#/components/schemas/LatestLocation"}}},"required":["tripId","serverTime","transport","pollIntervalSeconds","locations"]},"MemberTrackResponse":{"type":"object","properties":{"tripId":{"type":"string"},"userId":{"type":"string"},"user":{"$ref":"#/components/schemas/PublicUser"},"sessionIds":{"type":"array","items":{"type":"string"}},"geometry":{"$ref":"#/components/schemas/LineString"},"startedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"endedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"originalPointCount":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Points matching the filter in the database."},"returnedPointCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"simplified":{"type":"boolean"},"simplificationToleranceM":{"type":"number","minimum":0},"distanceM":{"type":"integer","minimum":0,"maximum":9007199254740991},"durationSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"lastSequenceNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["tripId","userId","user","sessionIds","geometry","startedAt","endedAt","originalPointCount","returnedPointCount","simplified","simplificationToleranceM","distanceM","durationSeconds","lastSequenceNumber"]},"Friend":{"type":"object","properties":{"friendshipId":{"type":"string"},"user":{"$ref":"#/components/schemas/PublicUser"},"status":{"$ref":"#/components/schemas/FriendshipStatus"},"since":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]}},"required":["friendshipId","user","status","since"]},"FriendPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Friend"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."}},"required":["items","nextCursor"]},"FriendRequest":{"type":"object","properties":{"id":{"type":"string"},"direction":{"type":"string","enum":["incoming","outgoing"]},"user":{"description":"The other party.","$ref":"#/components/schemas/PublicUser"},"status":{"$ref":"#/components/schemas/FriendshipStatus"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"respondedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]}},"required":["id","direction","user","status","createdAt","respondedAt"]},"FriendRequestPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/FriendRequest"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."}},"required":["items","nextCursor"]},"CreateFriendRequest":{"type":"object","properties":{"userId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}}},"RecentParticipant":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/PublicUser"},"sharedTripCount":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"lastTripAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"lastTripId":{"type":"string"},"friendshipStatus":{"anyOf":[{"$ref":"#/components/schemas/FriendshipStatus"},{"type":"null"}]}},"required":["user","sharedTripCount","lastTripAt","lastTripId","friendshipStatus"]},"RecentParticipantPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RecentParticipant"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."}},"required":["items","nextCursor"]},"TripMessage":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"author":{"$ref":"#/components/schemas/PublicUser"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Null once the author has deleted the message."},"deleted":{"type":"boolean"},"sentAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"receivedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"cursor":{"type":"string"},"mine":{"type":"boolean"}},"required":["id","tripId","author","body","deleted","sentAt","receivedAt","cursor","mine"]},"MessagePage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TripMessage"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."},"serverTime":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"pollIntervalSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"latestCursor":{"anyOf":[{"type":"string"},{"type":"null"}]},"ascending":{"type":"boolean"},"unreadCount":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["items","nextCursor","serverTime","pollIntervalSeconds","latestCursor","ascending","unreadCount"]},"SendMessageRequest":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":2000},"clientMessageId":{"type":"string","minLength":8,"maxLength":64},"sentAt":{"description":"Defaults to the server time of the request.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},"required":["body"]},"MarkReadRequest":{"type":"object","properties":{"cursor":{"type":"string","minLength":1,"maxLength":500}},"required":["cursor"]},"ReadReceipt":{"type":"object","properties":{"unreadCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"readAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]}},"required":["unreadCount","readAt"]},"SosAlertStatus":{"type":"string","enum":["ACTIVE","RESOLVED","CANCELLED"]},"SosPosition":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"accuracyM":{"anyOf":[{"type":"number"},{"type":"null"}]},"altitudeM":{"anyOf":[{"type":"number"},{"type":"null"}]},"recordedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]}},"required":["latitude","longitude","accuracyM","altitudeM","recordedAt"]},"SosAcknowledgement":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/PublicUser"},"acknowledgedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["user","acknowledgedAt"]},"SosAlert":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"user":{"description":"Who raised it.","$ref":"#/components/schemas/PublicUser"},"status":{"$ref":"#/components/schemas/SosAlertStatus"},"note":{"anyOf":[{"type":"string"},{"type":"null"}]},"position":{"anyOf":[{"$ref":"#/components/schemas/SosPosition"},{"type":"null"}]},"batteryLevel":{"anyOf":[{"type":"number"},{"type":"null"}]},"raisedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"ageSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"resolvedAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},{"type":"null"}]},"resolvedBy":{"anyOf":[{"$ref":"#/components/schemas/PublicUser"},{"type":"null"}]},"acknowledgements":{"type":"array","items":{"$ref":"#/components/schemas/SosAcknowledgement"}},"acknowledgedByMe":{"type":"boolean"},"mine":{"type":"boolean"}},"required":["id","tripId","user","status","note","position","batteryLevel","raisedAt","ageSeconds","resolvedAt","resolvedBy","acknowledgements","acknowledgedByMe","mine"]},"SosAlertPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SosAlert"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."},"serverTime":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"pollIntervalSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"activeCount":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["items","nextCursor","serverTime","pollIntervalSeconds","activeCount"]},"RaiseSosRequest":{"type":"object","properties":{"note":{"type":"string","maxLength":500},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"accuracyM":{"type":"number","minimum":0,"maximum":100000},"altitudeM":{"type":"number","minimum":-1000,"maximum":20000},"batteryLevel":{"type":"number","minimum":0,"maximum":1},"recordedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}}},"SosResolution":{"type":"string","enum":["RESOLVED","CANCELLED"]},"ResolveSosRequest":{"type":"object","properties":{"status":{"default":"RESOLVED","$ref":"#/components/schemas/SosResolution"}}},"CreateExpenseRequest":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":160},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}]},"amount":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"paidByUserId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"paidAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"splitMethod":{"default":"EQUAL","$ref":"#/components/schemas/SplitMethod"},"participants":{"minItems":1,"maxItems":100,"type":"array","items":{"$ref":"#/components/schemas/ExpenseParticipantInput"}}},"required":["title","amount"]},"UpdateExpenseRequest":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":160},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}]},"amount":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"paidByUserId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"paidAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"splitMethod":{"default":"EQUAL","$ref":"#/components/schemas/SplitMethod"},"participants":{"minItems":1,"maxItems":100,"type":"array","items":{"$ref":"#/components/schemas/ExpenseParticipantInput"}}}},"Expense":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"title":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"currency":{"type":"string"},"paidBy":{"$ref":"#/components/schemas/PublicUser"},"createdByUserId":{"type":"string"},"paidAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"splitMethod":{"$ref":"#/components/schemas/SplitMethod"},"hasReceipt":{"type":"boolean"},"receiptUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Short-lived signed URL, or null when no receipt."},"participants":{"type":"array","items":{"$ref":"#/components/schemas/ExpenseParticipant"}},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"}},"required":["id","tripId","title","description","amount","currency","paidBy","createdByUserId","paidAt","splitMethod","hasReceipt","receiptUrl","participants","createdAt","updatedAt"]},"ExpensePage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Expense"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` to fetch the next page. Null when exhausted."}},"required":["items","nextCursor"]},"ExpenseParticipant":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"user":{"$ref":"#/components/schemas/PublicUser"},"share":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"amountOwed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"amountPaid":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"settlementStatus":{"$ref":"#/components/schemas/SettlementStatus"}},"required":["id","userId","user","share","amountOwed","amountPaid","settlementStatus"]},"ExpenseParticipantInput":{"type":"object","properties":{"userId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"share":{"anyOf":[{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},{"type":"null"}]},"amount":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]}},"required":["userId"]},"ReceiptUploadResponse":{"type":"object","properties":{"expenseId":{"type":"string"},"receiptStorageKey":{"type":"string"},"receiptUrl":{"type":"string"},"sizeBytes":{"type":"integer","minimum":0,"maximum":9007199254740991},"contentType":{"type":"string"}},"required":["expenseId","receiptStorageKey","receiptUrl","sizeBytes","contentType"]},"Settlement":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"debtor":{"$ref":"#/components/schemas/PublicUser"},"creditor":{"$ref":"#/components/schemas/PublicUser"},"amount":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"currency":{"type":"string"},"status":{"$ref":"#/components/schemas/SettlementStatus"},"components":{"type":"array","items":{"$ref":"#/components/schemas/SettlementComponent"}},"payableByCaller":{"type":"boolean"}},"required":["id","tripId","debtor","creditor","amount","currency","status","components","payableByCaller"]},"SettlementComponent":{"type":"object","properties":{"expenseId":{"type":"string"},"expenseTitle":{"type":"string"},"expenseParticipantId":{"type":"string"},"amountOwed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"amountPaid":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["expenseId","expenseTitle","expenseParticipantId","amountOwed","amountPaid"]},"SettlementsResponse":{"type":"object","properties":{"tripId":{"type":"string"},"currency":{"type":"string"},"settlements":{"type":"array","items":{"$ref":"#/components/schemas/Settlement"}}},"required":["tripId","currency","settlements"]},"MarkPaidRequest":{"type":"object","properties":{"amount":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"paymentReference":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}]},"paidAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}}},"MarkPaidResponse":{"type":"object","properties":{"settlementId":{"type":"string"},"settledAmount":{"type":"integer","minimum":0,"maximum":9007199254740991},"fullySettled":{"type":"boolean"},"remaining":{"anyOf":[{"$ref":"#/components/schemas/Settlement"},{"type":"null"}]}},"required":["settlementId","settledAmount","fullySettled","remaining"]},"QrPaymentResponse":{"type":"object","properties":{"settlementId":{"type":"string"},"payload":{"type":"string"},"format":{"type":"string","const":"SPAYD_1.0"},"amount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"currency":{"type":"string"},"amountFormatted":{"type":"string"},"variableSymbol":{"anyOf":[{"type":"string"},{"type":"null"}]},"message":{"type":"string"},"iban":{"type":"string"},"accountDisplay":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Czech domestic form, e.g. 19-2000145399/0800."},"recipientName":{"type":"string"},"imageUrl":{"type":"string"}},"required":["settlementId","payload","format","amount","currency","amountFormatted","variableSymbol","message","iban","accountDisplay","recipientName","imageUrl"]},"ConfigurationIssue":{"type":"object","properties":{"variable":{"type":"string"},"message":{"type":"string"}},"required":["variable","message"]},"HealthCheck":{"type":"object","properties":{"status":{"type":"string","enum":["up","down","skipped"]},"latencyMs":{"type":"number"},"error":{"type":"string"},"issues":{"description":"Rejected environment variables. Names and reasons only, never values.","type":"array","items":{"$ref":"#/components/schemas/ConfigurationIssue"}}},"required":["status"]},"HealthResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"]},"version":{"type":"string"},"time":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"ISO 8601 instant in UTC, e.g. 2026-07-30T14:02:03.000Z"},"uptimeMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"checks":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/HealthCheck"}},"configuration":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["status","version","time","uptimeMs","checks","configuration"]}},"headers":{"x-request-id":{"description":"Echoed on every response. Quote it when reporting a problem.","schema":{"type":"string"}}}},"paths":{"/api/health":{"get":{"tags":["System"],"summary":"Liveness and readiness probe","security":[],"responses":{"200":{"description":"The API and its dependencies are healthy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"A required dependency is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/v1/auth/register":{"post":{"tags":["Authentication"],"summary":"Create an account and open a session","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"201":{"description":"Account created; tokens issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthSession"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The e-mail is already registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/login":{"post":{"tags":["Authentication"],"summary":"Exchange credentials for a token pair","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Tokens issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthSession"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Rotate a refresh token","description":"Refresh tokens are single-use. Presenting one that was already used revokes the entire rotation family and returns `REFRESH_TOKEN_REUSED` — treat that as \"the session is gone, sign in again\".","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshRequest"}}}},"responses":{"200":{"description":"A new token pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthSession"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/logout":{"post":{"tags":["Authentication"],"summary":"Sign out one device","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutRequest"}}}},"responses":{"200":{"description":"Tokens revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/logout-all":{"post":{"tags":["Authentication"],"summary":"Sign out every device","description":"Revokes all refresh tokens. Already-issued access tokens remain valid until they expire, which is why their lifetime is short.","responses":{"200":{"description":"Tokens revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["Authentication"],"summary":"The authenticated user and their devices","responses":{"200":{"description":"Current profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"The caller’s own profile","responses":{"200":{"description":"Current profile, including private bank details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"patch":{"tags":["Users"],"summary":"Update the caller’s profile","description":"Setting the Czech account parts derives and stores the matching IBAN. Account number and bank code must be supplied together.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMeRequest"}}}},"responses":{"200":{"description":"Updated profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/users/me/avatar":{"post":{"tags":["Users"],"summary":"Upload or replace the profile photo","requestBody":{"required":true,"description":"At most 5 MB. The real file type is verified from the bytes, not from the declared content type. When sending a raw body, pass the original name in `X-Filename`.","content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/heic":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Avatar stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarUploadResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The file is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Unsupported file type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/users/{userId}":{"get":{"tags":["Users"],"summary":"Public profile of any user","description":"Never includes e-mail, locale or bank details.","parameters":[{"name":"userId","in":"path","required":true,"description":"User id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Public profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUserProfile"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/users/{userId}/avatar":{"get":{"tags":["Users"],"summary":"Avatar bytes (authorised proxy)","description":"Fallback for storage drivers that cannot mint presigned URLs. Prefer the `avatarUrl` returned in profile payloads.","parameters":[{"name":"userId","in":"path","required":true,"description":"User id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Image bytes.","content":{"image/*":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips":{"post":{"tags":["Trips"],"summary":"Create a trip","description":"The creator becomes OWNER and an active member. The trip starts in `DRAFT`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTripRequest"}}}},"responses":{"201":{"description":"Trip created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["Trips"],"summary":"List the caller’s trips","parameters":[{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous page.","schema":{"description":"Opaque cursor from a previous page.","type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["DRAFT","PLANNED","ACTIVE","FINISHED","CANCELLED"]}},{"name":"scope","in":"query","required":false,"schema":{"default":"all","type":"string","enum":["all","upcoming","past"]}}],"responses":{"200":{"description":"One page of trips, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripSummaryPage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}":{"get":{"tags":["Trips"],"summary":"Trip detail","description":"Members only. Non-members receive 404 rather than 403, so trip ids cannot be probed. `joinCode` is present only for OWNER and ORGANIZER.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Trip detail with members.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"patch":{"tags":["Trips"],"summary":"Update trip details","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTripRequest"}}}},"responses":{"200":{"description":"Updated trip.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["Trips"],"summary":"Delete a trip (soft)","description":"OWNER only. The row is retained; the trip becomes invisible to everyone.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/invitations":{"post":{"tags":["Membership"],"summary":"Invite someone to the trip","description":"The single-use `inviteUrl` is returned only in this response — the token is stored hashed and cannot be read back.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvitationRequest"}}}},"responses":{"201":{"description":"Invitation created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invitation"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Already a member, or an invitation is pending.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["Membership"],"summary":"List invitations for the trip","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Invitations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationPage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/members":{"get":{"tags":["Membership"],"summary":"List trip members","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Members, owner and organizers first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripMemberPage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/members/{userId}":{"patch":{"tags":["Membership"],"summary":"Change a member’s role","description":"OWNER only. The OWNER role itself cannot be changed or granted here.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"description":"Member’s user id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMemberRequest"}}}},"responses":{"200":{"description":"Updated member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripMember"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["Membership"],"summary":"Remove a member, or leave the trip","description":"Passing your own user id leaves the trip. The OWNER can do neither — cancel or delete the trip instead.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"description":"Member’s user id.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Removed."},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/join/{joinCode}":{"get":{"tags":["Membership"],"summary":"Preview a trip from its join code","description":"The only endpoint that reveals anything about a trip to a non-member, and deliberately narrow: no notes, no member list, no locations.","parameters":[{"name":"joinCode","in":"path","required":true,"description":"Case-insensitive short code.","schema":{"type":"string"}}],"responses":{"200":{"description":"Limited preview.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripPreview"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The join code has expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["Membership"],"summary":"Join a trip using its code","description":"Idempotent: joining twice returns 200 with the existing membership.","parameters":[{"name":"joinCode","in":"path","required":true,"description":"Case-insensitive short code.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinTripRequest"}}}},"responses":{"200":{"description":"Already a member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"201":{"description":"Joined.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The code expired, or the trip can no longer be joined.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/invitations/{token}/accept":{"post":{"tags":["Membership"],"summary":"Accept an e-mail invitation","parameters":[{"name":"token","in":"path","required":true,"description":"Token from the invitation link.","schema":{"type":"string"}}],"responses":{"200":{"description":"Already a member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"201":{"description":"Joined.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The invitation has expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/publish":{"post":{"tags":["Trips"],"summary":"Publish a draft trip","description":"DRAFT → PLANNED. Members can see it; tracking is not yet running.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated trip.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The transition is not legal from the current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/start":{"post":{"tags":["Trips"],"summary":"Start the trip","description":"DRAFT or PLANNED → ACTIVE. Members may now start their own tracking.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated trip.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The transition is not legal from the current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/finish":{"post":{"tags":["Trips"],"summary":"Finish the trip","description":"ACTIVE → FINISHED. Terminal: data becomes historical.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated trip.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The transition is not legal from the current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/cancel":{"post":{"tags":["Trips"],"summary":"Cancel the trip","description":"DRAFT, PLANNED or ACTIVE → CANCELLED. Terminal.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated trip.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripDetail"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The transition is not legal from the current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/route":{"get":{"tags":["Routes"],"summary":"The planned route as GeoJSON","description":"Coordinates are always `[longitude, latitude]`.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"geometry","in":"query","required":false,"description":"`simplified` omits the full-fidelity line, which is much smaller on mobile data.","schema":{"default":"full","description":"`simplified` omits the full-fidelity line, which is much smaller on mobile data.","type":"string","enum":["full","simplified"]}}],"responses":{"200":{"description":"Route with metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripRoute"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The trip has no route, or is not visible to the caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["Routes"],"summary":"Delete the planned route","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/route/gpx":{"post":{"tags":["Routes"],"summary":"Upload or replace the route from a GPX file","description":"GPX 1.1. Reads `trk/trkseg/trkpt`, `rte/rtept` and `wpt`. Segment structure is preserved, so a gap renders as separate lines rather than a false straight jump. Limits: 10 MB, 200000 points. The original file is always retained.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"At most 10 MB. The real file type is verified from the bytes, not from the declared content type. When sending a raw body, pass the original name in `X-Filename`.","content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}},"application/gpx+xml":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"Route imported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteUploadResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The file is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Not a GPX file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"Parsed, but no usable track, or too many points.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["Routes"],"summary":"Download the original GPX file","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The original upload, byte for byte.","content":{"application/gpx+xml":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/tracking-sessions":{"post":{"tags":["Tracking"],"summary":"Start (or resume) your own tracking session","description":"Always creates a session for the AUTHENTICATED user; there is no way to start tracking on someone else’s behalf. Requires the trip to be ACTIVE. Calling again while a session is live returns that session, so app restarts are safe.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrackingSessionRequest"}}}},"responses":{"201":{"description":"Session started or resumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackingSession"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The trip is not ACTIVE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/tracking-sessions/{sessionId}/pause":{"post":{"tags":["Tracking"],"summary":"Pause your tracking session","description":"Stops publishing your position without ending the session.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"sessionId","in":"path","required":true,"description":"Your session id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackingSession"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No such session, or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The session has already ended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/tracking-sessions/{sessionId}/resume":{"post":{"tags":["Tracking"],"summary":"Resume your tracking session","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"sessionId","in":"path","required":true,"description":"Your session id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackingSession"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No such session, or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The session has already ended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/tracking-sessions/{sessionId}/finish":{"post":{"tags":["Tracking"],"summary":"Finish your tracking session","description":"Terminal. Recorded points are retained.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"sessionId","in":"path","required":true,"description":"Your session id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackingSession"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No such session, or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The session has already ended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/locations/batch":{"post":{"tags":["Tracking"],"summary":"Upload a batch of GPS points","description":"Idempotent on `(sessionId, sequenceNumber)`: re-sending a batch after a timeout is safe and changes nothing. At most 500 points per call. The client may delete its local queue up to `lastAcknowledgedSequenceNumber`, which is read back from the database rather than echoed. `recordedAt` is stored as sent but never trusted — the server also records its own `received_at`, and implausible timestamps come back in `rejected`.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationBatchRequest"}}}},"responses":{"200":{"description":"Batch processed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationBatchResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The trip or the session is not accepting points.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The batch is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/locations/latest":{"get":{"tags":["Tracking"],"summary":"Current positions of active members","description":"The polling endpoint that makes live tracking work without a persistent connection. Every entry carries `ageSeconds` and a derived `freshness` (LIVE <20 s, DELAYED <60 s, STALE <5 min, OFFLINE beyond), so a stale fix is never presented as current. Poll again after `pollIntervalSeconds`.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Latest known positions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestLocationsResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/members/{userId}/track":{"get":{"tags":["Tracking"],"summary":"A member’s recorded track","description":"Returned as a GeoJSON LineString, simplified if needed to fit `maxPoints`. Stored points are never modified. `distanceM` is computed from the full point set, so a simplified render still reports the true distance. Use `afterSequence` for incremental fetches during a live trip.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"description":"Member’s user id.","schema":{"type":"string","format":"uuid"}},{"name":"sessionId","in":"query","required":false,"description":"Restrict to one tracking session.","schema":{"description":"Restrict to one tracking session.","type":"string"}},{"name":"from","in":"query","required":false,"description":"ISO 8601 lower bound on recordedAt.","schema":{"description":"ISO 8601 lower bound on recordedAt.","type":"string"}},{"name":"to","in":"query","required":false,"description":"ISO 8601 upper bound on recordedAt.","schema":{"description":"ISO 8601 upper bound on recordedAt.","type":"string"}},{"name":"maxPoints","in":"query","required":false,"schema":{"default":2000,"type":"integer","minimum":2,"maximum":20000}},{"name":"afterSequence","in":"query","required":false,"schema":{"type":"integer","minimum":-1,"maximum":9007199254740991}}],"responses":{"200":{"description":"The member’s track.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberTrackResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/messages":{"get":{"tags":["Chat"],"summary":"The trip’s conversation","description":"Two modes, and `ascending` says which one answered.\n\nOpening the screen: omit `since`. Newest first; `nextCursor` walks backwards through the history.\nPolling it: pass `since=<latestCursor>` from the previous response. Oldest first, so the client appends.\n\nOrdering is always by the server’s `receivedAt`, never by the sender’s clock — a phone that comes out of a tunnel with a wrong clock and three queued messages cannot insert them into the middle of somebody else’s conversation. `sentAt` is carried for display only.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Older messages, from a previous `nextCursor`.","schema":{"description":"Older messages, from a previous `nextCursor`.","type":"string"}},{"name":"since","in":"query","required":false,"description":"Newer messages only, from a previous `latestCursor`. Excludes `cursor`.","schema":{"description":"Newer messages only, from a previous `latestCursor`. Excludes `cursor`.","type":"string"}}],"responses":{"200":{"description":"A page of the conversation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagePage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["Chat"],"summary":"Post a message","description":"Any active member may write; a finished or cancelled trip is closed. Send a `clientMessageId` to make the call idempotent — a retry after a lost response returns the existing message with 200 instead of posting a duplicate with 201.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"}}}},"responses":{"200":{"description":"This `clientMessageId` was already posted; the existing message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripMessage"}}}},"201":{"description":"Message posted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripMessage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/messages/read":{"post":{"tags":["Chat"],"summary":"Mark the conversation read","description":"Takes the cursor of the newest message the client has actually shown, not “now”: the difference is exactly the messages that arrived while the screen was open but below the fold. The marker only moves forward, so two devices cannot undo each other.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkReadRequest"}}}},"responses":{"200":{"description":"Marker updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadReceipt"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/messages/{messageId}":{"delete":{"tags":["Chat"],"summary":"Delete your own message","description":"Only the author — an organiser cannot delete somebody else’s words through this API. Returns the message with `body: null` and `deleted: true` rather than 204, so the client can render “message deleted” in place; the row stays so replies still have something to hang off.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"messageId","in":"path","required":true,"description":"Message id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripMessage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/sos":{"get":{"tags":["SOS"],"summary":"The trip’s alerts","description":"Members only, newest first. This is how an SOS reaches anybody today: there is no push channel, so this endpoint is polled — and on a shorter interval than anything else in the API, because here staleness is measured in the thing the feature exists to prevent. `activeCount` counts every live alert on the trip, not just this page. `ageSeconds` is computed from the server’s clock.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"`active` for live alerts only, `all` for the history too.","schema":{"default":"active","description":"`active` for live alerts only, `all` for the history too.","type":"string","enum":["active","all"]}}],"responses":{"200":{"description":"Alerts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SosAlertPage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["SOS"],"summary":"Raise an alert","description":"Every active member can, whatever their role.\n\nThe position is optional and comes from the caller’s own device. There is no endpoint that raises an alert for somebody else, and none that reads a position nobody chose to send — a cold GPS must never be the reason a call for help does not go out.\n\nAlready have a live alert on this trip? That one is returned with 200 instead of a second being created; the database enforces one live alert per member per trip. Pressing the button twice is what people do, and two alerts would split the attention of everyone trying to help.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RaiseSosRequest"}}}},"responses":{"200":{"description":"An alert was already live; that one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SosAlert"}}}},"201":{"description":"Alert raised.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SosAlert"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/sos/{alertId}/acknowledge":{"post":{"tags":["SOS"],"summary":"Acknowledge an alert","description":"“I have seen this, I am on my way.” The point is the person who raised it: an alert that disappears into a broadcast tells somebody sitting in a ditch nothing about whether help is coming. Idempotent. The raiser cannot acknowledge their own.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"alertId","in":"path","required":true,"description":"Alert id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Acknowledged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SosAlert"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/sos/{alertId}/resolve":{"post":{"tags":["SOS"],"summary":"Close an alert","description":"The raiser can always close their own. An organiser or the owner can close somebody else’s, because the person who raised it may be in no state to — and an alert nobody can clear stays on every screen forever. `CANCELLED` (false alarm) is reserved for the raiser: that is a statement about your own intent, and letting somebody else make it is how a real emergency ends up filed as a mistake.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"alertId","in":"path","required":true,"description":"Alert id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveSosRequest"}}}},"responses":{"200":{"description":"Closed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SosAlert"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/friends":{"get":{"tags":["Social"],"summary":"Accepted friends","parameters":[{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Friends.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendPage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/friends/{userId}":{"delete":{"tags":["Social"],"summary":"Unfriend someone","parameters":[{"name":"userId","in":"path","required":true,"description":"The friend’s user id.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Removed."},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/friend-requests":{"post":{"tags":["Social"],"summary":"Send a friend request","description":"If the other person already has a request pending towards you, this accepts it (200) instead of creating a mirror request.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFriendRequest"}}}},"responses":{"200":{"description":"A reciprocal request existed and was accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendRequest"}}}},"201":{"description":"Request sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendRequest"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Already friends, or a request is pending.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["Social"],"summary":"Pending friend requests","parameters":[{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"direction","in":"query","required":false,"schema":{"default":"all","type":"string","enum":["incoming","outgoing","all"]}}],"responses":{"200":{"description":"Pending requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendRequestPage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/friend-requests/{requestId}/accept":{"post":{"tags":["Social"],"summary":"Accept a friend request","parameters":[{"name":"requestId","in":"path","required":true,"description":"Friend request id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendRequest"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/friend-requests/{requestId}/decline":{"post":{"tags":["Social"],"summary":"Decline a friend request","parameters":[{"name":"requestId","in":"path","required":true,"description":"Friend request id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Declined.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FriendRequest"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/recent-participants":{"get":{"tags":["Social"],"summary":"People you have shared trips with","description":"Derived from completed and ongoing shared trips, annotated with the current friendship state.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Recent participants.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecentParticipantPage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/expenses":{"post":{"tags":["Expenses"],"summary":"Record a shared expense","description":"`amount` is an INTEGER in minor currency units — 450,50 Kč is `45050`. Omitting `participants` splits across every active member. Allocations always sum to exactly `amount`; `EQUAL` gives the indivisible remainder to the first participants, `SHARES` uses the largest-remainder method.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExpenseRequest"}}}},"responses":{"201":{"description":"Expense created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"400":{"description":"Validation failed, or a participant is not an active trip member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The split does not add up to the total.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["Expenses"],"summary":"List trip expenses","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"paidByUserId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Expenses, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpensePage"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/expenses/{expenseId}":{"get":{"tags":["Expenses"],"summary":"Expense detail","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"expenseId","in":"path","required":true,"description":"Expense id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Expense.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"patch":{"tags":["Expenses"],"summary":"Update an expense","description":"Changing the amount, payer, split method or participants recomputes every allocation, so the parts always sum to the total. Only the creator, the payer, or the trip owner may edit.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"expenseId","in":"path","required":true,"description":"Expense id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExpenseRequest"}}}},"responses":{"200":{"description":"Updated expense.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["Expenses"],"summary":"Delete an expense (soft)","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"expenseId","in":"path","required":true,"description":"Expense id.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/expenses/{expenseId}/receipt":{"post":{"tags":["Expenses"],"summary":"Attach or replace the receipt","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"expenseId","in":"path","required":true,"description":"Expense id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"At most 10 MB. The real file type is verified from the bytes, not from the declared content type. When sending a raw body, pass the original name in `X-Filename`.","content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/heic":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Receipt stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptUploadResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The file is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Unsupported file type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["Expenses"],"summary":"Receipt bytes (authorised proxy)","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"expenseId","in":"path","required":true,"description":"Expense id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Receipt bytes.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/settlements":{"get":{"tags":["Expenses"],"summary":"Who owes whom","description":"Derived from the expense rows on every request, so it can never disagree with them. Crossing debts between the same two people are netted into one figure. Contains no bank details.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Outstanding net debts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettlementsResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/settlements/{settlementId}/mark-paid":{"post":{"tags":["Expenses"],"summary":"Confirm a settlement was paid","description":"Only the debtor or the creditor may confirm. Partial payments are supported; the amount is applied across the underlying expense participations, each capped at its own outstanding balance.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"settlementId","in":"path","required":true,"description":"Settlement id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkPaidRequest"}}}},"responses":{"200":{"description":"Payment recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkPaidResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The payment exceeds the outstanding amount.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/settlements/{settlementId}/qr-payment":{"get":{"tags":["Expenses"],"summary":"Czech QR payment (SPAYD) for a debt you owe","description":"The ONLY endpoint that discloses another user’s bank details, and only to the DEBTOR of this exact settlement. Trip membership is not sufficient. Render `payload` to a QR code on the device; the PNG endpoint is a convenience.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"settlementId","in":"path","required":true,"description":"Settlement id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"SPAYD payload and display metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrPaymentResponse"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The caller is not the debtor of this settlement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"The creditor has not set up a bank account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/trips/{tripId}/settlements/{settlementId}/qr-payment.png":{"get":{"tags":["Expenses"],"summary":"QR payment rendered as PNG","description":"Same authorisation rule as the JSON endpoint: debtor only.","parameters":[{"name":"tripId","in":"path","required":true,"description":"Trip id.","schema":{"type":"string","format":"uuid"}},{"name":"settlementId","in":"path","required":true,"description":"Settlement id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"PNG image of the SPAYD payload.","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed or the request is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, expired or invalid access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Authenticated, but not allowed to perform this action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The resource does not exist, or the caller may not know that it does.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}},"x-error-codes":["VALIDATION_ERROR","MALFORMED_JSON","UNAUTHORIZED","INVALID_CREDENTIALS","TOKEN_EXPIRED","TOKEN_INVALID","REFRESH_TOKEN_REUSED","FORBIDDEN","NOT_FOUND","METHOD_NOT_ALLOWED","CONFLICT","EMAIL_ALREADY_REGISTERED","RATE_LIMITED","PAYLOAD_TOO_LARGE","UNSUPPORTED_MEDIA_TYPE","INTERNAL_ERROR","NOT_IMPLEMENTED","USER_NOT_FOUND","TRIP_NOT_FOUND","TRIP_INVALID_STATE_TRANSITION","TRIP_NOT_ACTIVE","TRIP_MEMBERSHIP_REQUIRED","TRIP_ALREADY_MEMBER","TRIP_OWNER_CANNOT_LEAVE","MEMBER_NOT_FOUND","JOIN_CODE_NOT_FOUND","JOIN_CODE_EXPIRED","INVITATION_NOT_FOUND","INVITATION_EXPIRED","ROUTE_NOT_FOUND","GPX_INVALID","GPX_TOO_LARGE","GPX_NO_USABLE_TRACK","GPX_TOO_MANY_POINTS","TRACKING_SESSION_NOT_FOUND","TRACKING_SESSION_NOT_ACTIVE","TRACKING_SESSION_ALREADY_ACTIVE","TRACKING_SESSION_FINISHED","BATCH_TOO_LARGE","FRIEND_REQUEST_NOT_FOUND","FRIEND_REQUEST_EXISTS","FRIENDSHIP_NOT_FOUND","CANNOT_BEFRIEND_SELF","MESSAGE_NOT_FOUND","SOS_ALERT_NOT_FOUND","SOS_ALERT_NOT_ACTIVE","EXPENSE_NOT_FOUND","EXPENSE_SPLIT_MISMATCH","EXPENSE_INVALID_PARTICIPANT","SETTLEMENT_NOT_FOUND","BANK_DETAILS_MISSING","STORAGE_NOT_CONFIGURED","FILE_NOT_FOUND","FILE_INVALID_TYPE"]}