openapi: 3.1.0
info:
  title: Travelmode Trips & Itinerary API
  version: "1.0.0"
  description: >
    Read and mutate a trip's itinerary, run revision-tracked timeline
    operations, manage collaborator permissions, drive the AI proposal
    accept/reject workflow, and resolve a user's primary trip.


    Unlike the developer-key APIs (Weather, Visa, Platform), these endpoints
    use the **end-user auth model**: a logged-in session cookie or a mobile /
    extension bearer token identifies the caller, and access is gated by
    **trip membership and role** (owner / planner / editor / viewer), not by a
    `tm_` API key. See the Authentication guide for details.


    These endpoints return resource objects directly (no `{ data, meta }`
    envelope). Errors are returned as `{ "error": <string | array> }`.
servers:
  - url: https://api.travelmode.ai
    description: Production
  - url: https://{host}
    description: Self-hosted / Replit deployment. Replace `{host}` with your deployed domain.
    variables:
      host:
        default: your-app-domain.replit.app
security:
  - BearerToken: []
  - SessionCookie: []
tags:
  - name: Itinerary
    description: Read-only views of a trip's events, diffs, and summary.
  - name: Operations
    description: Revision-tracked, atomic timeline mutations.
  - name: Permissions
    description: Collaborator role management (owner-only mutations).
  - name: Proposals
    description: AI proposal listing, creation, and accept/reject workflow.
  - name: Users
    description: A user's primary trip.
paths:
  /v1/trips/{tripId}/itinerary:
    get:
      tags: [Itinerary]
      summary: Get a trip's full itinerary
      description: >
        Returns the trip's current revision and all of its active events,
        ordered by start time. Requires trip membership (any role).
      parameters:
        - $ref: "#/components/parameters/TripId"
      responses:
        "200":
          description: The trip's itinerary at its current revision.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Itinerary"
              example:
                trip_id: 42
                revision: 7
                status: planning
                event_count: 2
                events:
                  - id: 901
                    type: flight
                    title: "JFK → FCO"
                    date: "2026-07-01"
                    time: "18:30"
                    location: "John F. Kennedy International Airport"
                    booking_status: planned_unbooked
                    importance: anchor
                  - id: 902
                    type: accommodation
                    title: "Hotel Quirinale"
                    date: "2026-07-02"
                    time: "15:00"
                    location: "Rome, Italy"
                    booking_status: booked
                    importance: important
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/trips/{tripId}/itinerary/diff:
    get:
      tags: [Itinerary]
      summary: Diff a trip's itinerary since a revision
      description: >
        Returns the timeline operations applied since `since_revision` plus the
        net per-event diffs, so a client can fast-forward its local cache
        without re-fetching the whole itinerary. Requires trip membership.
      parameters:
        - $ref: "#/components/parameters/TripId"
        - name: since_revision
          in: query
          required: true
          description: The revision the caller has already applied locally.
          schema:
            type: integer
            minimum: 0
      responses:
        "200":
          description: The operations and per-event diffs since the given revision.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ItineraryDiff"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/trips/{tripId}/itinerary/summary:
    get:
      tags: [Itinerary]
      summary: Get a compact itinerary summary
      description: >
        Returns a compact, agent-friendly digest of the itinerary: counts by
        event type, geographic bounds, detected scheduling conflicts, and a
        compact event list. Requires trip membership.
      parameters:
        - $ref: "#/components/parameters/TripId"
      responses:
        "200":
          description: A compact summary of the trip.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ItinerarySummary"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/trips/{tripId}/ops:
    post:
      tags: [Operations]
      summary: Apply a revision-tracked timeline operation
      description: >
        Applies a single atomic operation to the trip's timeline. The caller
        MUST supply the `base_revision` they last observed; if the trip has
        advanced past it, the server rejects the operation with `409` and a
        `STALE_REVISION` code so the client can re-read and retry
        (compare-and-set). Requires trip membership with a mutating role
        (owner / planner / editor).
      parameters:
        - $ref: "#/components/parameters/TripId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperationRequest"
            example:
              base_revision: 7
              type: move_event
              payload:
                event_id: 901
                date: "2026-07-02"
                time: "20:00"
      responses:
        "200":
          description: The operation was applied and the trip's revision advanced.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperationResult"
              example:
                success: true
                trip_id: 42
                new_revision: 8
                operation: move_event
                result:
                  affected_event_ids: [901]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: >
            The operation could not be applied. Either the supplied
            `base_revision` is stale (`STALE_REVISION` — re-read and retry) or
            the operation failed validation against the current timeline.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperationConflict"
              example:
                error: Revision conflict
                code: STALE_REVISION
                current_revision: 9
                base_revision: 7
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/trips/{tripId}/permissions:
    get:
      tags: [Permissions]
      summary: List a trip's collaborators
      description: Returns every permission row for the trip. Requires trip membership.
      parameters:
        - $ref: "#/components/parameters/TripId"
      responses:
        "200":
          description: The trip's collaborators and their roles.
          content:
            application/json:
              schema:
                type: object
                required: [trip_id, permissions]
                properties:
                  trip_id:
                    type: integer
                  permissions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Permission"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Permissions]
      summary: Grant or update a collaborator's role
      description: >
        Grants a role to a user, or updates their existing role. **Owner-only**:
        only the trip owner may change permissions.
      parameters:
        - $ref: "#/components/parameters/TripId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id, role]
              properties:
                user_id:
                  type: string
                  description: The user to grant the role to.
                role:
                  $ref: "#/components/schemas/Role"
            example:
              user_id: usr_8c1f
              role: planner
      responses:
        "200":
          description: The role was granted or updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PermissionGrantResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Only the trip owner may change permissions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Only the trip owner can manage permissions
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/trips/{tripId}/permissions/{userId}:
    delete:
      tags: [Permissions]
      summary: Revoke a collaborator
      description: >
        Removes a user's permission row from the trip. **Owner-only.** The
        owner's own permission cannot be revoked.
      parameters:
        - $ref: "#/components/parameters/TripId"
        - $ref: "#/components/parameters/UserIdPath"
      responses:
        "200":
          description: The collaborator was revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  trip_id:
                    type: integer
                  user_id:
                    type: string
                  revoked:
                    type: boolean
        "400":
          description: The owner's permission cannot be revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Cannot revoke the trip owner
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/trips/{tripId}/proposals:
    get:
      tags: [Proposals]
      summary: List AI proposals for a trip
      description: >
        Returns proposals raised by the planning agent for this trip,
        optionally filtered by status. Requires trip membership.
      parameters:
        - $ref: "#/components/parameters/TripId"
        - name: status
          in: query
          required: false
          description: Filter by proposal status.
          schema:
            $ref: "#/components/schemas/ProposalStatus"
      responses:
        "200":
          description: The trip's proposals.
          content:
            application/json:
              schema:
                type: object
                required: [trip_id, proposals]
                properties:
                  trip_id:
                    type: integer
                  proposals:
                    type: array
                    items:
                      $ref: "#/components/schemas/Proposal"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Proposals]
      summary: Create an AI proposal
      description: >
        Records a new proposal (a set of candidate entities plus optional
        ranked scheduling options) for member review. Requires trip membership.
      parameters:
        - $ref: "#/components/parameters/TripId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProposalRequest"
      responses:
        "201":
          description: The proposal was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Proposal"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/trips/{tripId}/proposals/{proposalId}:
    get:
      tags: [Proposals]
      summary: Get a single proposal
      description: Returns the full proposal, including entities and ranked options. Requires trip membership.
      parameters:
        - $ref: "#/components/parameters/TripId"
        - $ref: "#/components/parameters/ProposalId"
      responses:
        "200":
          description: The proposal.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Proposal"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Proposals]
      summary: Accept or reject a proposal
      description: >
        Resolves a pending proposal. The `action` query parameter selects the
        outcome. `accept` applies the proposal (optionally choosing one of its
        ranked options) and returns the created event ids; `reject` records an
        optional reason. Requires trip membership. A proposal that is already
        resolved or expired returns `400`.
      parameters:
        - $ref: "#/components/parameters/TripId"
        - $ref: "#/components/parameters/ProposalId"
        - name: action
          in: query
          required: true
          description: The resolution to apply.
          schema:
            type: string
            enum: [accept, reject]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                selected_option_id:
                  type: string
                  description: "For `accept`: the ranked option to apply."
                reason:
                  type: string
                  description: "For `reject`: an optional human-readable reason."
      responses:
        "200":
          description: The proposal was accepted or rejected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProposalResolution"
              examples:
                accepted:
                  value:
                    proposal_id: 7b3c0e6e-2c1a-4f0e-9d2b-1a2b3c4d5e6f
                    status: accepted
                    created_event_ids: [903, 904]
                    accepted_at: "2026-06-20T12:00:00.000Z"
                rejected:
                  value:
                    proposal_id: 7b3c0e6e-2c1a-4f0e-9d2b-1a2b3c4d5e6f
                    status: rejected
                    rejection_reason: "Hotel is outside budget"
                    rejected_at: "2026-06-20T12:00:00.000Z"
        "400":
          description: Invalid action, or the proposal is already resolved or expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/users/{userId}/trip:
    get:
      tags: [Users]
      summary: Get the caller's primary trip
      description: >
        Returns the caller's primary trip. **Self-only**: `userId` must match
        the authenticated user; requesting another user's trip returns `403`.
      parameters:
        - $ref: "#/components/parameters/UserIdPath"
      responses:
        "200":
          description: The user's primary trip.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrimaryTrip"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Users]
      summary: Get or create the caller's primary trip
      description: >
        Returns the caller's primary trip, creating it if none exists.
        **Self-only.** Responds `201` when a trip was created, `200` when an
        existing trip was returned.
      parameters:
        - $ref: "#/components/parameters/UserIdPath"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Optional name for a newly created trip.
      responses:
        "200":
          description: An existing primary trip was returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrimaryTrip"
        "201":
          description: A new primary trip was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrimaryTrip"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"
components:
  securitySchemes:
    BearerToken:
      type: http
      scheme: bearer
      description: >
        A mobile access token or browser-extension session token, sent as
        `Authorization: Bearer <token>`. This is the credential the
        reference's Try It console uses (cookies are not sent from the
        in-browser console).
    SessionCookie:
      type: apiKey
      in: cookie
      name: session_token
      description: >
        The first-party web session cookie set at login. Sent automatically by
        the browser for same-origin requests.
  parameters:
    TripId:
      name: tripId
      in: path
      required: true
      description: Numeric trip id.
      schema:
        type: integer
        minimum: 1
    UserIdPath:
      name: userId
      in: path
      required: true
      description: The user id.
      schema:
        type: string
    ProposalId:
      name: proposalId
      in: path
      required: true
      description: The proposal's UUID (`proposal_id`).
      schema:
        type: string
  responses:
    BadRequest:
      description: The request was malformed or failed validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Invalid trip id
    Unauthorized:
      description: No valid session cookie or bearer token was supplied.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Authentication required
    Forbidden:
      description: The caller is authenticated but lacks access to this trip or resource.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Access denied
    NotFound:
      description: The trip or resource was not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Trip not found
    ServerError:
      description: An unexpected error occurred.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Internal server error
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          description: >
            A human-readable error message, or — for validation failures — an
            array of structured Zod issues.
          oneOf:
            - type: string
            - type: array
              items:
                type: object
                additionalProperties: true
    Role:
      type: string
      enum: [owner, planner, editor, viewer]
      description: A collaborator's role on a trip, in descending privilege order.
    TripStatus:
      type: string
      enum: [planning, upcoming, travel_mode, completed, cancelled, documenting, published]
    Event:
      type: object
      description: A single itinerary event.
      properties:
        id:
          type: integer
        type:
          type: string
          description: "e.g. flight, accommodation, activity, food, local_transit, train, free_block."
        subtype:
          type: [string, "null"]
        title:
          type: string
        date:
          type: string
          description: Local date (YYYY-MM-DD).
        time:
          type: string
          description: Local time (HH:MM).
        start_date_time:
          type: [string, "null"]
          format: date-time
        end_date_time:
          type: [string, "null"]
          format: date-time
        description:
          type: string
        location:
          type: string
        price:
          type: [string, "null"]
        rating:
          type: [number, "null"]
        booking_status:
          type: string
          enum: [not_needed, planned_unbooked, offer_pending, booked, booking_failed]
        flexibility:
          type: string
          enum: [fixed, flexible, suggestion]
        importance:
          type: string
          enum: [anchor, important, optional]
        revision:
          type: integer
      additionalProperties: true
    Itinerary:
      type: object
      required: [trip_id, revision, status, events, event_count]
      properties:
        trip_id:
          type: integer
        revision:
          type: integer
        status:
          $ref: "#/components/schemas/TripStatus"
        event_count:
          type: integer
        events:
          type: array
          items:
            $ref: "#/components/schemas/Event"
    ItineraryDiff:
      type: object
      required: [trip_id, since_revision, current_revision]
      properties:
        trip_id:
          type: integer
        since_revision:
          type: integer
        current_revision:
          type: integer
        operations:
          type: array
          items:
            $ref: "#/components/schemas/TimelineOperation"
        event_diffs:
          type: array
          items:
            type: object
            additionalProperties: true
    ItinerarySummary:
      type: object
      required: [trip_id, revision, status, event_count]
      properties:
        trip_id:
          type: integer
        revision:
          type: integer
        status:
          $ref: "#/components/schemas/TripStatus"
        event_count:
          type: integer
        events_by_type:
          type: object
          additionalProperties:
            type: integer
        trip_bounds:
          type: [object, "null"]
          additionalProperties: true
        conflicts:
          type: array
          items:
            type: object
            additionalProperties: true
        conflict_count:
          type: integer
        compact_events:
          type: array
          items:
            type: object
            additionalProperties: true
    TimelineOperation:
      type: object
      properties:
        op_id:
          type: string
        operation_type:
          type: string
        base_revision:
          type: integer
        result_revision:
          type: integer
        actor_type:
          type: string
          enum: [user, agent, system]
        status:
          type: string
          enum: [applied, rejected, rolled_back]
        affected_event_ids:
          type: array
          items:
            type: integer
      additionalProperties: true
    OperationRequest:
      type: object
      required: [base_revision, type, payload]
      properties:
        base_revision:
          type: integer
          minimum: 0
          description: The revision the caller last observed (compare-and-set token).
        type:
          type: string
          description: >
            The operation type, e.g. `add_event`, `move_event`, `update_event`,
            `remove_event`, `reorder`. The accepted set is enforced by the
            timeline engine.
        payload:
          type: object
          description: Operation-specific fields (shape depends on `type`).
          additionalProperties: true
    OperationResult:
      type: object
      required: [success, trip_id, new_revision, operation]
      properties:
        success:
          type: boolean
        trip_id:
          type: integer
        new_revision:
          type: integer
        operation:
          type: string
        result:
          type: object
          additionalProperties: true
    OperationConflict:
      type: object
      required: [error]
      properties:
        error:
          type: string
        code:
          type: string
          enum: [STALE_REVISION, VALIDATION_FAILED]
        current_revision:
          type: integer
        base_revision:
          type: integer
    Permission:
      type: object
      properties:
        user_id:
          type: string
        role:
          $ref: "#/components/schemas/Role"
        granted_by_user_id:
          type: [string, "null"]
        granted_at:
          type: string
          format: date-time
      additionalProperties: true
    PermissionGrantResult:
      type: object
      properties:
        trip_id:
          type: integer
        user_id:
          type: string
        role:
          $ref: "#/components/schemas/Role"
        granted_by:
          type: [string, "null"]
    ProposalStatus:
      type: string
      enum: [pending, accepted, rejected, expired]
    Proposal:
      type: object
      properties:
        proposal_id:
          type: string
        trip_id:
          type: integer
        actor_agent_id:
          type: [string, "null"]
        actor_type:
          type: string
          enum: [agent, system]
        entities:
          type: array
          items:
            type: object
            additionalProperties: true
        options:
          type: [array, "null"]
          items:
            type: object
            additionalProperties: true
        target_event_ids:
          type: [array, "null"]
          items:
            type: integer
        status:
          $ref: "#/components/schemas/ProposalStatus"
        expires_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
      additionalProperties: true
    CreateProposalRequest:
      type: object
      required: [entities]
      properties:
        actor_agent_id:
          type: string
        entities:
          type: array
          items:
            type: object
            additionalProperties: true
        options:
          type: array
          items:
            type: object
            additionalProperties: true
        target_event_ids:
          type: array
          items:
            type: integer
        expires_at:
          type: string
          format: date-time
      additionalProperties: true
    ProposalResolution:
      type: object
      required: [proposal_id, status]
      properties:
        proposal_id:
          type: string
        status:
          type: string
          enum: [accepted, rejected]
        created_event_ids:
          type: array
          items:
            type: integer
        accepted_at:
          type: string
          format: date-time
        rejection_reason:
          type: [string, "null"]
        rejected_at:
          type: string
          format: date-time
    PrimaryTrip:
      type: object
      properties:
        trip_id:
          type: integer
        owner_user_id:
          type: string
        revision:
          type: integer
        name:
          type: string
        status:
          $ref: "#/components/schemas/TripStatus"
        created:
          type: boolean
          description: Present on POST; true when a new trip was created.
      additionalProperties: true
