{
  "openapi": "3.1.0",
  "info": {
    "title": "TheTripMap API",
    "version": "1.0.0",
    "description": "Public API for discovering and creating trip itineraries on TheTripMap. Search existing public trips, get full trip details, and create temporary AI-generated trips without authentication. Temporary trips auto-delete after 3 days and can be claimed by registered users.\n\n## Workflow for AI Assistants\n\n1. **Search for existing trips**: Use GET /api/trips with a search query to find public trips for inspiration or reference.\n2. **Get full trip details**: Use GET /api/trips/:id to retrieve the complete itinerary including all pins, paths, notes, and expenses.\n3. **Create a new trip**: Use POST /api/trips with a complete trip JSON payload. No authentication required. The response includes a preview URL and trip ID.\n4. **Share the preview URL**: The preview URL (e.g. https://thetripmap.com/trip/:id) shows the trip on a map and allows a user to claim it.\n\n## Trip Categories\n- unclassified: Default. Use for trips that don't fit a specific activity.\n- hiking: Walking, backpacking, trekking.\n- river: Canoe, kayak, raft trips on rivers.\n- lake: Canoe, kayak, boat trips on lakes.\n- biking: Mountain biking, road cycling, bikepacking.\n- motor: Motorized vehicle trips.\n- alpine: Mountaineering, ski touring, high-altitude.\n\n## Pin Categories\n- general: Default. Generic waypoints, landmarks, meeting points.\n- parking: Trailheads, access points, vehicle parking.\n- campsite: Overnight camping locations.\n- poi: Points of interest -- viewpoints, waterfalls, historical markers.\n- portage: Canoe/kayak carry-over points between water bodies.\n- water: Water sources or access points.\n\n## Temporary Trip Lifecycle\n\n- Created without an owner, viewable by anyone with the link\n- Auto-delete after 3 days if not claimed\n- Can be claimed by a registered user via POST /api/trips/:id/claim (requires authentication)\n- Never appear in public search results or community feeds\n- AI assistants should preserve trip data locally if persistence is needed beyond 3 days"
  },
  "servers": [
    {
      "url": "https://thetripmap.com/functions/v1/api",
      "description": "TheTripMap public API endpoint"
    }
  ],
  "paths": {
    "/trips": {
      "get": {
        "summary": "Search public trips",
        "description": "Search for public trips by text query, optionally filtered by category and geographic bounding box. Returns trip summaries with location, stats, and public URLs.",
        "operationId": "searchTrips",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Search query (searches trip titles, addresses, owner usernames, and pin addresses). Leave empty to search by geographic area only.",
            "required": false,
            "schema": { "type": "string" }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by trip category. One of: unclassified, hiking, river, lake, biking, motor, alpine.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["unclassified", "hiking", "river", "lake", "biking", "motor", "alpine"]
            }
          },
          {
            "name": "min_lat",
            "in": "query",
            "description": "Minimum latitude for geographic bounding box search. All four bbox parameters (min_lat, max_lat, min_lng, max_lng) must be provided to activate geographic filtering.",
            "required": false,
            "schema": { "type": "number", "minimum": -90, "maximum": 90 }
          },
          {
            "name": "max_lat",
            "in": "query",
            "description": "Maximum latitude for geographic bounding box search",
            "required": false,
            "schema": { "type": "number", "minimum": -90, "maximum": 90 }
          },
          {
            "name": "min_lng",
            "in": "query",
            "description": "Minimum longitude for geographic bounding box search",
            "required": false,
            "schema": { "type": "number", "minimum": -180, "maximum": 180 }
          },
          {
            "name": "max_lng",
            "in": "query",
            "description": "Maximum longitude for geographic bounding box search",
            "required": false,
            "schema": { "type": "number", "minimum": -180, "maximum": 180 }
          },
          {
            "name": "min_route_count",
            "in": "query",
            "description": "Minimum number of routes (sub-projects) the trip must have. Use to find trips with detailed route data.",
            "required": false,
            "schema": { "type": "integer", "minimum": 0 }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of results (1-100, default 20)",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Pagination offset (default 0)",
            "required": false,
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "trips": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/TripSummary" }
                    },
                    "total": { "type": "integer", "description": "Total matching results for pagination" },
                    "limit": { "type": "integer" },
                    "offset": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a temporary trip",
        "description": "Create a new temporary trip from a complete trip JSON payload. No authentication required. The trip will auto-delete after 3 days if not claimed. Maximum 200 items per trip.\n\nItems are processed in array order. The zero-based array index is used for cross-referencing:\n- Paths use start_pin_index and end_pin_index to reference pin positions in the array\n- Notes and expenses use parent_index to reference their parent pin position (null = trip-level)",
        "operationId": "createTrip",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/TripPayload" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Temporary trip created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid", "description": "The new trip's project ID" },
                    "preview_url": { "type": "string", "description": "Relative URL to view the trip (e.g. /trip/:id)" },
                    "message": { "type": "string" },
                    "expires_in_days": { "type": "integer", "description": "Days until auto-deletion (3)" }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": { "type": "string" },
                    "validation_errors": { "type": "array", "items": { "type": "string" } }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/trips/{id}": {
      "get": {
        "summary": "Get full trip detail",
        "description": "Retrieve the complete itinerary of a public trip, including all pins with coordinates, paths with waypoints, notes, and expenses. Each item in the items array includes type-specific data in the 'data' field -- see the PathData, ExpenseData, and ImageData schemas for details.",
        "operationId": "getTrip",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Trip/project UUID",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Full trip detail",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TripDetail" }
              }
            }
          },
          "404": {
            "description": "Trip not found or not public",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/temporary/{id}": {
      "get": {
        "summary": "Get a temporary trip for preview",
        "description": "Retrieve a temporary (AI-generated) trip by its ID. Only works for trips marked as temporary and not yet expired.",
        "operationId": "getTemporaryTrip",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Temporary trip project UUID",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Temporary trip detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "trip": {
                      "type": "object",
                      "properties": {
                        "project": {
                          "type": "object",
                          "properties": {
                            "id": { "type": "string", "format": "uuid" },
                            "title": { "type": "string" },
                            "description": { "type": "string" },
                            "is_temporary": { "type": "boolean" },
                            "expires_at": { "type": "string", "format": "date-time" }
                          }
                        },
                        "items": { "type": "array", "items": { "$ref": "#/components/schemas/TimelineItem" } }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": { "description": "Temporary trip not found" },
          "410": { "description": "Temporary trip has expired" }
        }
      }
    },
    "/trips/{id}/claim": {
      "post": {
        "summary": "Claim a temporary trip",
        "description": "Transfer ownership of a temporary trip to the authenticated user. The trip becomes a normal private project. Requires a valid Supabase JWT in the Authorization header.",
        "operationId": "claimTrip",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Temporary trip project UUID",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Trip claimed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "message": { "type": "string" },
                    "redirect_url": { "type": "string", "description": "URL to edit the claimed project" }
                  }
                }
              }
            }
          },
          "401": { "description": "Authentication required" },
          "404": { "description": "Temporary trip not found" },
          "410": { "description": "Temporary trip has expired" }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Health check and endpoint listing",
        "operationId": "healthCheck",
        "responses": {
          "200": {
            "description": "API status and available endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string" },
                    "endpoints": { "type": "array", "items": { "type": "string" } }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Use the public anon key as the Bearer token. This key is safe to expose publicly and provides read-only access to public trips plus the ability to create temporary trips. Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp4aHNmaHFjbnNzdWd6eW1hcGJiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE2OTE3NjYsImV4cCI6MjA4NzI2Nzc2Nn0.bnpxlvQIFV1PwggQ1aYkrZol_a6KP9p56mcrGpdk23E"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" }
        }
      },
      "TripSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "title": { "type": "string" },
          "description": { "type": "string" },
          "address": { "type": "string", "nullable": true, "description": "Location address if set" },
          "category": { "type": "string", "description": "Trip category: unclassified, hiking, river, lake, biking, motor, or alpine" },
          "latitude": { "type": "number", "nullable": true, "description": "Center latitude of the trip's pins" },
          "longitude": { "type": "number", "nullable": true, "description": "Center longitude of the trip's pins" },
          "owner_handle": { "type": "string", "nullable": true, "description": "Owner's URL handle" },
          "owner_username": { "type": "string", "nullable": true, "description": "Owner's display username" },
          "pin_count": { "type": "integer", "description": "Total number of pins in the trip" },
          "route_count": { "type": "integer", "description": "Number of routes (sub-projects) in the trip" },
          "total_distance_km": { "type": "number", "description": "Total distance of all non-optional routes in kilometers" },
          "public_url": { "type": "string", "nullable": true, "description": "Relative URL to view the trip (e.g. /username/trip-slug)" }
        }
      },
      "TripDetail": {
        "type": "object",
        "properties": {
          "trip": {
            "type": "object",
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "title": { "type": "string" },
              "description": { "type": "string", "nullable": true },
              "address": { "type": "string", "nullable": true },
              "category": { "type": "string", "description": "Trip category: unclassified, hiking, river, lake, biking, motor, or alpine" },
              "owner": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "username": { "type": "string", "nullable": true },
                  "handle": { "type": "string", "nullable": true }
                }
              },
              "public_url": { "type": "string", "nullable": true },
              "items": {
                "type": "array",
                "items": { "$ref": "#/components/schemas/TimelineItem" },
                "description": "Ordered array of trip items (pins, paths, notes, expenses). Sorted by sort_order."
              }
            }
          }
        }
      },
      "TimelineItem": {
        "type": "object",
        "description": "A single item in a trip's timeline. The 'data' field contains type-specific structured data -- see PathData for path items, ExpenseData for expense items, and ImageData for image items. For pins and notes, data is typically empty or null.",
        "properties": {
          "id": { "type": "string", "format": "uuid", "description": "Unique item ID" },
          "type": { "type": "string", "enum": ["pin", "path", "image", "note", "expense"], "description": "Item type determines which fields are populated" },
          "sort_order": { "type": "integer", "description": "Display order in the timeline (ascending)" },
          "title": { "type": "string", "nullable": true, "description": "Item title" },
          "description": { "type": "string", "nullable": true, "description": "Longer description text" },
          "address": { "type": "string", "nullable": true, "description": "Address or landmark name (pins only)" },
          "url": { "type": "string", "nullable": true, "description": "URL associated with the item (notes only)" },
          "pin_category": { "type": "string", "nullable": true, "enum": ["general", "parking", "campsite", "poi", "portage", "water"], "description": "Pin category (pins only). general=default, parking=trailhead/vehicle, campsite=overnight, poi=scenic/notable, portage=canoe carry-over, water=water source" },
          "is_optional": { "type": "boolean", "nullable": true, "description": "Whether the item is optional. For pins: marks alternate/contingency stops. For paths: excludes from total distance calculation." },
          "latitude": { "type": "number", "nullable": true, "description": "WGS84 decimal degrees (pins only)" },
          "longitude": { "type": "number", "nullable": true, "description": "WGS84 decimal degrees (pins only)" },
          "data": { "type": "object", "description": "Type-specific data. For path items: PathData (waypoints, distance_km, travel_time, routing info). For expense items: ExpenseData (cost, currency). For image items: ImageData (image_url, dimensions, caption). For pins and notes: typically empty or null.", "oneOf": [
            { "$ref": "#/components/schemas/PathData" },
            { "$ref": "#/components/schemas/ExpenseData" },
            { "$ref": "#/components/schemas/ImageData" },
            { "type": "object", "description": "Empty or null for pin and note items" }
          ]},
          "parent_id": { "type": "string", "format": "uuid", "nullable": true, "description": "ID of the parent item (for notes/expenses attached to a pin). Null for trip-level notes/expenses and for all pins and paths." }
        }
      },
      "PathData": {
        "type": "object",
        "description": "Structured data for path (route) items. Contains the route shape, distance, and optional routing metadata.",
        "properties": {
          "waypoints": {
            "type": "array",
            "description": "Array of [latitude, longitude] pairs defining the route shape on the map. Minimum 2 points. Include intermediate points to trace curves, river bends, or trail switchbacks.",
            "items": {
              "type": "array",
              "items": { "type": "number" },
              "minItems": 2,
              "maxItems": 2
            },
            "minItems": 2
          },
          "distance_km": { "type": "number", "description": "Route distance in kilometers. Contributes to the trip's total_distance_km." },
          "travel_time": { "type": "string", "nullable": true, "description": "Estimated travel time (e.g. '2h 30m')" },
          "start_pin_id": { "type": "string", "format": "uuid", "nullable": true, "description": "ID of the start pin if connected" },
          "end_pin_id": { "type": "string", "format": "uuid", "nullable": true, "description": "ID of the end pin if connected" },
          "auto_connection": { "type": "boolean", "nullable": true, "description": "True if auto-generated as a connection between adjacent pins" },
          "parent_child_connection": { "type": "boolean", "nullable": true, "description": "True if connecting a parent project to a sub-project (route)" },
          "intermediate_waypoints": {
            "type": "array",
            "nullable": true,
            "description": "Additional waypoints between start and end for route customization",
            "items": {
              "type": "array",
              "items": { "type": "number" },
              "minItems": 2,
              "maxItems": 2
            }
          },
          "routed_waypoints": {
            "type": "array",
            "nullable": true,
            "description": "Waypoints generated by a routing service (road/trail following)",
            "items": {
              "type": "array",
              "items": { "type": "number" },
              "minItems": 2,
              "maxItems": 2
            }
          },
          "routing_profile": { "type": "string", "nullable": true, "description": "Routing profile used (e.g. 'driving', 'walking', 'cycling')" },
          "is_precision_route": { "type": "boolean", "nullable": true, "description": "True if using precision pin positions for enhanced accuracy" },
          "precision_route_pin_positions": {
            "type": "object",
            "nullable": true,
            "description": "Precision start/end pin positions",
            "properties": {
              "start": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
              "end": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 }
            }
          }
        }
      },
      "ExpenseData": {
        "type": "object",
        "description": "Structured data for expense items.",
        "properties": {
          "cost": { "type": "number", "minimum": 0, "description": "Cost amount" },
          "currency": { "type": "string", "default": "USD", "description": "ISO 4217 currency code (e.g. USD, CAD, EUR, GBP, AUD, JPY)" }
        }
      },
      "ImageData": {
        "type": "object",
        "description": "Structured data for image items. Images are typically created through the app UI rather than the API.",
        "properties": {
          "storage_path": { "type": "string", "nullable": true, "description": "Internal storage path" },
          "image_url": { "type": "string", "nullable": true, "description": "Public URL of the image" },
          "is_external": { "type": "boolean", "nullable": true, "description": "True if hosted externally" },
          "thumbnail_path": { "type": "string", "nullable": true, "description": "Internal thumbnail storage path" },
          "width": { "type": "integer", "nullable": true, "description": "Image width in pixels" },
          "height": { "type": "integer", "nullable": true, "description": "Image height in pixels" },
          "caption": { "type": "string", "nullable": true, "description": "User-provided caption" }
        }
      },
      "TripPayload": {
        "type": "object",
        "required": ["project", "items"],
        "properties": {
          "project": {
            "type": "object",
            "required": ["title", "description"],
            "properties": {
              "title": { "type": "string", "description": "Short, evocative trip title" },
              "description": { "type": "string", "description": "1-2 sentence description of the trip" }
            }
          },
          "items": {
            "type": "array",
            "description": "Ordered array of trip items (max 200). Items are processed in order, and indices for paths/notes/expenses reference positions in this array. Zero-based indexing: the first item is index 0.",
            "items": { "$ref": "#/components/schemas/TripItem" }
          }
        }
      },
      "TripItem": {
        "type": "object",
        "required": ["type", "title"],
        "discriminator": { "propertyName": "type" },
        "oneOf": [
          { "$ref": "#/components/schemas/PinItem" },
          { "$ref": "#/components/schemas/PathItem" },
          { "$ref": "#/components/schemas/NoteItem" },
          { "$ref": "#/components/schemas/ExpenseItem" }
        ]
      },
      "PinItem": {
        "type": "object",
        "required": ["type", "title", "latitude", "longitude"],
        "properties": {
          "type": { "type": "string", "enum": ["pin"] },
          "title": { "type": "string", "description": "Short name (e.g. 'Trailhead Parking', 'Campsite 1', 'Lookout Point')" },
          "description": { "type": "string", "nullable": true, "description": "Longer description of terrain, difficulty, amenities, or what to expect" },
          "address": { "type": "string", "nullable": true, "description": "Real address or landmark name" },
          "latitude": { "type": "number", "description": "WGS84 decimal degrees (-90 to 90)" },
          "longitude": { "type": "number", "description": "WGS84 decimal degrees (-180 to 180)" },
          "pin_category": { "type": "string", "enum": ["general", "parking", "campsite", "poi", "portage", "water"], "default": "general", "description": "Pin category. general=default, parking=trailhead/vehicle, campsite=overnight, poi=scenic/notable, portage=canoe carry-over, water=water source" },
          "is_optional": { "type": "boolean", "default": false, "description": "If true, marks the pin as optional (for alternate campsites, optional side trips, contingency stops)" }
        }
      },
      "PathItem": {
        "type": "object",
        "required": ["type", "title", "waypoints"],
        "properties": {
          "type": { "type": "string", "enum": ["path"] },
          "title": { "type": "string", "description": "Short name (e.g. 'Paddle to Campsite', 'Hike to Summit')" },
          "description": { "type": "string", "nullable": true, "description": "Terrain details, difficulty, estimated time, navigation notes" },
          "start_pin_index": { "type": "integer", "nullable": true, "description": "Zero-based index of the start pin in the items array. If omitted, the path is a standalone route." },
          "end_pin_index": { "type": "integer", "nullable": true, "description": "Zero-based index of the end pin in the items array. If omitted, the path is a standalone route." },
          "waypoints": {
            "type": "array",
            "description": "Array of [latitude, longitude] pairs defining the route shape (minimum 2). Include intermediate points to trace curves, river bends, or trail switchbacks. Example: [[45.589, -78.637], [45.590, -78.638], [45.591, -78.640]]",
            "items": {
              "type": "array",
              "items": { "type": "number" },
              "minItems": 2,
              "maxItems": 2
            },
            "minItems": 2
          },
          "distance_km": { "type": "number", "nullable": true, "description": "Estimated distance in kilometers. Contributes to the trip's total_distance_km. May be auto-calculated if omitted." },
          "is_optional": { "type": "boolean", "default": false, "description": "If true, the route is excluded from total distance calculation. Use for alternate routes, side trips, or contingency paths." }
        }
      },
      "NoteItem": {
        "type": "object",
        "required": ["type", "title"],
        "properties": {
          "type": { "type": "string", "enum": ["note"] },
          "title": { "type": "string", "description": "Short title (e.g. 'Water Levels', 'Bear Safety', 'Permit Info')" },
          "description": { "type": "string", "nullable": true, "description": "Full text of the note (safety warnings, weather notes, permit requirements, gear recommendations, etc.)" },
          "url": { "type": "string", "nullable": true, "description": "A valid http or https URL related to the note (e.g. park permit page, weather forecast)" },
          "parent_index": { "type": "integer", "nullable": true, "description": "Zero-based index of the parent pin in the items array. If omitted or null, the note is trip-level (not attached to any specific pin)." }
        }
      },
      "ExpenseItem": {
        "type": "object",
        "required": ["type", "title", "cost"],
        "properties": {
          "type": { "type": "string", "enum": ["expense"] },
          "title": { "type": "string", "description": "Short description (e.g. 'Park Permit', 'Canoe Rental', 'Gas', 'Food')" },
          "description": { "type": "string", "nullable": true, "description": "Additional details about the expense" },
          "cost": { "type": "number", "minimum": 0, "description": "Cost amount (must be >= 0)" },
          "currency": { "type": "string", "default": "USD", "description": "ISO 4217 currency code (e.g. USD, CAD, EUR, GBP, AUD, JPY)" },
          "parent_index": { "type": "integer", "nullable": true, "description": "Zero-based index of the parent pin in the items array. If omitted or null, the expense is trip-level." }
        }
      }
    }
  }
}
