openapi: 3.1.0
info:
  title: Travelmode Events API
  version: "1.0.0"
  description: >
    Create, read, update, and delete itinerary events. An event (a flight,
    stay, activity, meal, transit leg, and so on) can be attached to one or
    more trips through the many-to-many trip-events join, so the same booking
    can appear on every traveller's trip.


    These endpoints use the **end-user auth model** (session cookie or mobile /
    extension bearer token), not a `tm_` developer key. Authorization is
    layered:

      * **Read** (`GET`) and **create** (`POST`) require membership of the
        relevant trip(s) — `POST` checks access to *every* trip you attach to.
      * **Update** and **delete** are restricted to the event's **planner**
        (`plannerUserId`), the single user empowered to change or remove an
        event globally across all trips it is linked to.


    Responses are resource objects, not a `{ data, meta }` envelope. Errors are
    returned as `{ "error": <string | array> }`. A soft-deleted event returns
    `410 Gone`.
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: Events
    description: Itinerary event lifecycle.
paths:
  /v1/events:
    post:
      tags: [Events]
      summary: Create an event
      description: >
        Creates an event and attaches it to the trips named in
        `attach.trip_ids`. The caller must have access to **every** trip in that
        list; if any check fails the whole request is rejected with `403`. The
        creating user becomes the event's planner.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventRequest"
            example:
              type: activity
              title: "Colosseum guided tour"
              date: "2026-07-03"
              time: "10:00"
              description: "Skip-the-line small group tour"
              location: "Piazza del Colosseo, Rome"
              importance: important
              attach:
                trip_ids: [42]
      responses:
        "201":
          description: The event was created and attached.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedEvent"
              example:
                event_id: 905
                type: activity
                title: "Colosseum guided tour"
                date: "2026-07-03"
                time: "10:00"
                planner_user_id: usr_8c1f
                attached_trips: [42]
                revision: 0
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: The caller lacks access to one of the trips in `attach.trip_ids`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Access denied for trip 42
        "500":
          $ref: "#/components/responses/ServerError"
  /v1/events/{eventId}:
    get:
      tags: [Events]
      summary: Get an event
      description: >
        Returns a single event. Requires membership of at least one trip the
        event is attached to. A soft-deleted event returns `410`.
      parameters:
        - $ref: "#/components/parameters/EventId"
      responses:
        "200":
          description: The event.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Event"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "500":
          $ref: "#/components/responses/ServerError"
    patch:
      tags: [Events]
      summary: Update an event
      description: >
        Applies a partial update to an event. **Planner-only**: only the user
        recorded as the event's planner (`plannerUserId`) may update it. The
        update advances the event's `revision` and impacts every trip the event
        is attached to. A soft-deleted event returns `410`.
      parameters:
        - $ref: "#/components/parameters/EventId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEventRequest"
            example:
              title: "Colosseum & Roman Forum tour"
              time: "09:30"
      responses:
        "200":
          description: The event was updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpdatedEvent"
              example:
                event_id: 905
                title: "Colosseum & Roman Forum tour"
                revision: 1
                impacted_trips: [42]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Only the event's planner may update it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Only the event planner can update this event
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "500":
          $ref: "#/components/responses/ServerError"
    delete:
      tags: [Events]
      summary: Delete an event
      description: >
        Soft-deletes an event (sets `deletedAt`) and any paired events (for
        example the return leg of a round-trip flight). **Planner-only.**
        Deleting an already-deleted event is idempotent and reports
        `already_deleted: true`.
      parameters:
        - $ref: "#/components/parameters/EventId"
        - name: reason
          in: query
          required: false
          description: Optional human-readable delete reason, recorded on the event.
          schema:
            type: string
      responses:
        "200":
          description: The event was deleted (or was already deleted).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteEventResult"
              examples:
                deleted:
                  value:
                    event_id: 905
                    deleted: true
                    deleted_at: "2026-06-20T12:00:00.000Z"
                    impacted_trips: [42]
                    paired_events_deleted: [906]
                alreadyDeleted:
                  value:
                    event_id: 905
                    deleted: true
                    already_deleted: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Only the event's planner may delete it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Only the event planner can delete this event
        "404":
          $ref: "#/components/responses/NotFound"
        "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:
    EventId:
      name: eventId
      in: path
      required: true
      description: Numeric event id.
      schema:
        type: integer
        minimum: 1
  responses:
    BadRequest:
      description: The request was malformed or failed validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Invalid event 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 event.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Access denied
    NotFound:
      description: The event was not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Event not found
    Gone:
      description: The event has been soft-deleted.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Event has been deleted
    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
    EventType:
      type: string
      description: "Event kind: flight, accommodation, activity, food, local_transit, train, free_block."
    Event:
      type: object
      description: A single itinerary event.
      properties:
        id:
          type: integer
        type:
          $ref: "#/components/schemas/EventType"
        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
        location_data:
          type: [object, "null"]
          additionalProperties: true
        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]
        planner_user_id:
          type: [string, "null"]
        revision:
          type: integer
      additionalProperties: true
    CreateEventRequest:
      type: object
      required: [type, title, date, time, description, location, attach]
      properties:
        type:
          $ref: "#/components/schemas/EventType"
        subtype:
          type: string
        title:
          type: string
        date:
          type: string
          description: Local date (YYYY-MM-DD).
        time:
          type: string
          description: Local time (HH:MM).
        description:
          type: string
        location:
          type: string
        price:
          type: string
        importance:
          type: string
          enum: [anchor, important, optional]
        flexibility:
          type: string
          enum: [fixed, flexible, suggestion]
        attach:
          type: object
          required: [trip_ids]
          properties:
            trip_ids:
              type: array
              minItems: 1
              items:
                type: integer
                minimum: 1
      additionalProperties: true
    CreatedEvent:
      type: object
      required: [event_id, attached_trips]
      properties:
        event_id:
          type: integer
        type:
          $ref: "#/components/schemas/EventType"
        title:
          type: string
        date:
          type: string
        time:
          type: string
        planner_user_id:
          type: [string, "null"]
        attached_trips:
          type: array
          items:
            type: integer
        revision:
          type: integer
    UpdateEventRequest:
      type: object
      description: A partial set of mutable event fields. At least one field is required.
      properties:
        title:
          type: string
        date:
          type: string
        time:
          type: string
        description:
          type: string
        location:
          type: string
        price:
          type: string
        importance:
          type: string
          enum: [anchor, important, optional]
        flexibility:
          type: string
          enum: [fixed, flexible, suggestion]
        booking_status:
          type: string
          enum: [not_needed, planned_unbooked, offer_pending, booked, booking_failed]
      additionalProperties: true
    UpdatedEvent:
      type: object
      required: [event_id, revision]
      properties:
        event_id:
          type: integer
        title:
          type: string
        revision:
          type: integer
        impacted_trips:
          type: array
          items:
            type: integer
    DeleteEventResult:
      type: object
      required: [event_id, deleted]
      properties:
        event_id:
          type: integer
        deleted:
          type: boolean
        already_deleted:
          type: boolean
        deleted_at:
          type: string
          format: date-time
        impacted_trips:
          type: array
          items:
            type: integer
        paired_events_deleted:
          type: array
          items:
            type: integer
