{"openapi":"3.1.0","info":{"title":"Pub Crawl","description":"Save a list of places to eat/drink as a named crawl. Each crawl gets a shareable Leaflet map and a GeoJSON file. Stops without lat/lon are geocoded from their address. Write endpoints need `Authorization: Bearer <token>` (or `X-API-Key: <token>`). Tokens have a scope: `create` tokens can create crawls and append stops to crawls they created; `admin` tokens can also replace and delete any crawl.","version":"1.1.0"},"paths":{"/api/crawls":{"get":{"tags":["crawls"],"summary":"List Crawls","operationId":"list_crawls_api_crawls_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CrawlSummary"},"type":"array","title":"Response List Crawls Api Crawls Get"}}}}}},"post":{"tags":["crawls"],"summary":"Create Crawl","description":"Create a crawl. If the slug is taken a free one is chosen (`-2`, `-3`, ...);\nthe response says which slug was used. Scope: create or admin.","operationId":"create_crawl_api_crawls_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlIn"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlLinks"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]},{"APIKeyHeader":[]}]}},"/api/crawls/{slug}":{"get":{"tags":["crawls"],"summary":"Get Crawl","operationId":"get_crawl_api_crawls__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Crawl"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["crawls"],"summary":"Replace Crawl","description":"Create or fully replace a crawl at this slug. Scope: admin.","operationId":"replace_crawl_api_crawls__slug__put","security":[{"HTTPBearer":[]},{"APIKeyHeader":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlLinks"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["crawls"],"summary":"Delete Crawl","description":"Delete a crawl. Scope: admin.","operationId":"delete_crawl_api_crawls__slug__delete","security":[{"HTTPBearer":[]},{"APIKeyHeader":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/crawls/{slug}/stops":{"post":{"tags":["crawls"],"summary":"Append Stop","description":"Append one stop to the end of a crawl. Scope: admin, or a create token that created this crawl.","operationId":"append_stop_api_crawls__slug__stops_post","security":[{"HTTPBearer":[]},{"APIKeyHeader":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StopIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlLinks"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/crawl/{slug}.geojson":{"get":{"tags":["files"],"summary":"Crawl Geojson","operationId":"crawl_geojson_crawl__slug__geojson_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/crawl/{slug}-map.html":{"get":{"tags":["files"],"summary":"Crawl Map","operationId":"crawl_map_crawl__slug__map_html_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Crawl":{"properties":{"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"date":{"type":"string","title":"Date","default":""},"author":{"type":"string","title":"Author","default":""},"stops":{"items":{"$ref":"#/components/schemas/Stop"},"type":"array","title":"Stops"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"created_by":{"type":"string","title":"Created By","default":""},"updated_by":{"type":"string","title":"Updated By","default":""}},"type":"object","required":["slug","title","stops"],"title":"Crawl"},"CrawlIn":{"properties":{"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title","examples":["Mission Dive Bar Crawl"]},"slug":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Slug","description":"URL slug; derived from title if omitted (e.g. 'mission-dive-bar-crawl')."},"date":{"type":"string","maxLength":100,"title":"Date","description":"Free-form display date.","default":"","examples":["Thu Oct 1, 2026"]},"author":{"type":"string","maxLength":100,"title":"Author","default":"","examples":["Pete"]},"stops":{"items":{"$ref":"#/components/schemas/StopIn"},"type":"array","maxItems":50,"minItems":1,"title":"Stops","description":"Stops in crawl order."}},"type":"object","required":["title","stops"],"title":"CrawlIn"},"CrawlLinks":{"properties":{"slug":{"type":"string","title":"Slug"},"map":{"type":"string","title":"Map"},"geojson":{"type":"string","title":"Geojson"},"api":{"type":"string","title":"Api"}},"type":"object","required":["slug","map","geojson","api"],"title":"CrawlLinks"},"CrawlSummary":{"properties":{"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"date":{"type":"string","title":"Date"},"stops":{"type":"integer","title":"Stops"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"links":{"$ref":"#/components/schemas/CrawlLinks"}},"type":"object","required":["slug","title","date","stops","updated_at","links"],"title":"CrawlSummary"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"Stop":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name","examples":["Zeitgeist"]},"address":{"type":"string","maxLength":300,"minLength":1,"title":"Address","description":"Full street address including city; used for geocoding and map links.","examples":["199 Valencia St, San Francisco, CA"]},"notes":{"type":"string","maxLength":1000,"title":"Notes","default":"","examples":["Thu 2 PM - 12 AM, beer garden, cheap pitchers"]},"lat":{"type":"number","title":"Lat"},"lon":{"type":"number","title":"Lon"}},"type":"object","required":["name","address","lat","lon"],"title":"Stop"},"StopIn":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name","examples":["Zeitgeist"]},"address":{"type":"string","maxLength":300,"minLength":1,"title":"Address","description":"Full street address including city; used for geocoding and map links.","examples":["199 Valencia St, San Francisco, CA"]},"notes":{"type":"string","maxLength":1000,"title":"Notes","default":"","examples":["Thu 2 PM - 12 AM, beer garden, cheap pitchers"]},"lat":{"anyOf":[{"type":"number","maximum":90.0,"minimum":-90.0},{"type":"null"}],"title":"Lat","description":"Optional; geocoded from address if omitted."},"lon":{"anyOf":[{"type":"number","maximum":180.0,"minimum":-180.0},{"type":"null"}],"title":"Lon","description":"Optional; geocoded from address if omitted."}},"type":"object","required":["name","address"],"title":"StopIn"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","description":"Authorization: Bearer <token>","scheme":"bearer"},"APIKeyHeader":{"type":"apiKey","description":"Alternative to the Authorization header","in":"header","name":"X-API-Key"}}}}