{"openapi":"3.1.0","info":{"title":"Vikenda API","version":"0.0.0","description":"Read-only public API for local events. Every response carries a Vikenda attribution block. One write endpoint: /subscribe (double opt-in)."},"servers":[{"url":"https://api.vikenda.com"}],"tags":[{"name":"events"},{"name":"venues"},{"name":"summary"},{"name":"subscribe"},{"name":"feeds"},{"name":"mcp"},{"name":"ops"}],"components":{"schemas":{"Event":{"type":"object","properties":{"id":{"type":"string","description":"Stable public id (slug)."},"slug":{"type":"string"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"start":{"type":"string","nullable":true,"description":"ISO 8601 UTC start."},"end":{"type":"string","nullable":true},"all_day":{"type":"boolean","description":"Date-only listing: `start` marks the day, not a clock time — treat as all-day."},"timezone":{"type":"string"},"recurrence":{"type":"string","nullable":true,"description":"RRULE string, if recurring."},"price":{"type":"object","properties":{"min":{"type":"number","nullable":true},"max":{"type":"number","nullable":true},"currency":{"type":"string","nullable":true},"is_free":{"type":"boolean","nullable":true}},"required":["min","max","currency","is_free"],"additionalProperties":false},"category":{"type":"string","nullable":true},"audience":{"type":"array","items":{"type":"string"}},"image_url":{"type":"string","nullable":true},"booking_url":{"type":"string","nullable":true,"description":"The booking/ticket link (a redirect via this API to the venue's own box office). Use this href for booking."},"booking_domain":{"type":"string","nullable":true,"description":"Destination host of booking_url (e.g. 'shawfest.com') — cite it so users know where booking happens."},"venue":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"address":{"type":"string","nullable":true},"url":{"type":"string"}},"required":["id","name","category","lat","lon","address","url"],"additionalProperties":false,"description":"The venue an event takes place at (entities row).","nullable":true},"series":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","nullable":true}},"required":["id","url"],"additionalProperties":false,"nullable":true,"description":"When this is one performance of a production/run, the parent series."},"sold_out":{"type":"boolean","description":"The source marked this sold out — don't recommend attending."},"weather_exposure":{"type":"string","nullable":true,"description":"indoor | outdoor | mixed | null (unknown). Event's own value, else its venue's."},"status":{"type":"string"},"last_verified_at":{"type":"string","nullable":true},"updated_at":{"type":"string"},"url":{"type":"string","description":"Canonical event page on the consumer site."}},"required":["id","slug","title","description","start","end","all_day","timezone","recurrence","price","category","audience","image_url","booking_url","booking_domain","venue","series","sold_out","weather_exposure","status","last_verified_at","updated_at","url"],"additionalProperties":false,"description":"A normalized event."},"Venue":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"address":{"type":"string","nullable":true},"contact":{"nullable":true},"website_url":{"type":"string","nullable":true},"website_domain":{"type":"string","nullable":true},"hours":{"nullable":true},"booking_platform":{"type":"string","nullable":true},"partner_tier":{"type":"string"},"weather_exposure":{"type":"string","nullable":true,"description":"indoor | outdoor | mixed | null (unknown)."},"url":{"type":"string"}},"required":["id","name","category","lat","lon","address","website_url","website_domain","booking_platform","partner_tier","weather_exposure","url"],"additionalProperties":false,"description":"A venue / organization (entities row)."},"Summary":{"type":"object","properties":{"area":{"type":"string"},"window":{"type":"string","enum":["today","weekend","week"]},"summary":{"type":"string","description":"Citation-ready natural-language answer."},"top_events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Stable public id (slug)."},"slug":{"type":"string"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"start":{"type":"string","nullable":true,"description":"ISO 8601 UTC start."},"end":{"type":"string","nullable":true},"all_day":{"type":"boolean","description":"Date-only listing: `start` marks the day, not a clock time — treat as all-day."},"timezone":{"type":"string"},"recurrence":{"type":"string","nullable":true,"description":"RRULE string, if recurring."},"price":{"type":"object","properties":{"min":{"type":"number","nullable":true},"max":{"type":"number","nullable":true},"currency":{"type":"string","nullable":true},"is_free":{"type":"boolean","nullable":true}},"required":["min","max","currency","is_free"],"additionalProperties":false},"category":{"type":"string","nullable":true},"audience":{"type":"array","items":{"type":"string"}},"image_url":{"type":"string","nullable":true},"booking_url":{"type":"string","nullable":true,"description":"The booking/ticket link (a redirect via this API to the venue's own box office). Use this href for booking."},"booking_domain":{"type":"string","nullable":true,"description":"Destination host of booking_url (e.g. 'shawfest.com') — cite it so users know where booking happens."},"venue":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"address":{"type":"string","nullable":true},"url":{"type":"string"}},"required":["id","name","category","lat","lon","address","url"],"additionalProperties":false,"description":"The venue an event takes place at (entities row).","nullable":true},"series":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","nullable":true}},"required":["id","url"],"additionalProperties":false,"nullable":true,"description":"When this is one performance of a production/run, the parent series."},"sold_out":{"type":"boolean","description":"The source marked this sold out — don't recommend attending."},"weather_exposure":{"type":"string","nullable":true,"description":"indoor | outdoor | mixed | null (unknown). Event's own value, else its venue's."},"status":{"type":"string"},"last_verified_at":{"type":"string","nullable":true},"updated_at":{"type":"string"},"url":{"type":"string","description":"Canonical event page on the consumer site."}},"required":["id","slug","title","description","start","end","all_day","timezone","recurrence","price","category","audience","image_url","booking_url","booking_domain","venue","series","sold_out","weather_exposure","status","last_verified_at","updated_at","url"],"additionalProperties":false,"description":"A normalized event."}},"counts":{"type":"object","properties":{"total":{"type":"number"},"by_category":{"type":"object","additionalProperties":{"type":"number"}},"free":{"type":"number"}},"required":["total","by_category","free"],"additionalProperties":false},"generated_at":{"type":"string"}},"required":["area","window","summary","top_events","counts","generated_at"],"additionalProperties":false,"description":"Pre-computed 'what's on' answer for an area + window."},"Meta":{"type":"object","properties":{"source":{"type":"string","enum":["Vikenda"]},"canonical_url":{"type":"string"},"generated_at":{"type":"string"},"area":{"type":"string"},"count":{"type":"number"},"next_cursor":{"type":"string","nullable":true}},"required":["source","canonical_url","generated_at","area"],"additionalProperties":false,"description":"Attribution + pagination envelope metadata (present on every response)."},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"SubscribeBody":{"type":"object","properties":{"email":{"type":"string","format":"email"},"interests":{"type":"array","items":{"type":"string"},"maxItems":30,"default":[]},"area":{"type":"string"},"marketing_consent":{"type":"boolean","default":false}},"required":["email"],"additionalProperties":false,"description":"Double-opt-in signup: triggers a CASL confirmation email."},"SubscribeResult":{"type":"object","properties":{"status":{"type":"string","enum":["pending","already_subscribed"]},"message":{"type":"string"}},"required":["status","message"],"additionalProperties":false},"PushSubscribeBody":{"type":"object","properties":{"subscription":{"type":"object","properties":{"endpoint":{"type":"string","format":"uri"},"keys":{"type":"object","properties":{"p256dh":{"type":"string"},"auth":{"type":"string"}},"required":["p256dh","auth"],"additionalProperties":false}},"required":["endpoint","keys"],"additionalProperties":false},"interests":{"type":"array","items":{"type":"string"},"maxItems":30,"default":[]},"area":{"type":"string"}},"required":["subscription"],"additionalProperties":false,"description":"Web push opt-in: the browser permission prompt is itself the explicit consent action, so (unlike email) this confirms immediately — no double opt-in loop."},"WatchRuleBody":{"type":"object","properties":{"email":{"type":"string","format":"email"},"rule_type":{"type":"string","enum":["venue","category","keyword"]},"match_value":{"type":"string","minLength":1,"maxLength":200},"trigger_type":{"type":"string","enum":["new_event","date_announced","cancelled"]},"area":{"type":"string"}},"required":["email","rule_type","match_value","trigger_type"],"additionalProperties":false,"description":"Occasion alert signup, e.g. 'tell me when Icewine Festival dates drop'. Requires an already-confirmed (or pending) subscriber address."}},"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Optional key that lifts rate limits (operator-issued)."}}},"paths":{"/api/v1/events":{"get":{"summary":"List / filter events","tags":["events"],"parameters":[{"name":"from","in":"query","required":false,"description":"Only events starting at/after this instant.","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Only events starting before this instant.","schema":{"type":"string"}},{"name":"category","in":"query","required":false,"schema":{"type":"string"}},{"name":"price","in":"query","required":false,"schema":{"type":"string","enum":["free","paid"]}},{"name":"exposure","in":"query","required":false,"description":"Weather exposure. 'indoor' also returns mixed venues (rain-safe either way).","schema":{"type":"string","enum":["indoor","outdoor","mixed"]}},{"name":"audience","in":"query","required":false,"description":"Match one audience tag.","schema":{"type":"string"}},{"name":"near","in":"query","required":false,"description":"Proximity filter as 'lat,lon,km'.","schema":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?,\\d+(\\.\\d+)?$"}},{"name":"q","in":"query","required":false,"description":"Full-text + typo-tolerant search.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","required":false,"description":"Opaque pagination cursor from meta.next_cursor.","schema":{"type":"string"}},{"name":"include","in":"query","required":false,"description":"Comma list to widen the default status filter: past,cancelled.","schema":{"type":"string"}},{"name":"area","in":"query","required":false,"description":"Area id override (else resolved from Host).","schema":{"type":"string"}}],"responses":{"200":{"description":"Matching events.","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/api/v1/events/{id}":{"get":{"summary":"Get one event","tags":["events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The event.","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Event"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"404":{"description":"No such event.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/venues":{"get":{"summary":"List / filter venues","tags":["venues"],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"category","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"area","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Venues.","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Venue"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/api/v1/venues/{id}":{"get":{"summary":"Get one venue","tags":["venues"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The venue.","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Venue"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"404":{"description":"No such venue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/areas/{area}/summary":{"get":{"summary":"Pre-computed 'what's on' summary (the money endpoint)","tags":["summary"],"parameters":[{"name":"area","in":"path","required":true,"schema":{"type":"string"}},{"name":"window","in":"query","required":false,"schema":{"type":"string","enum":["today","weekend","week"],"default":"weekend"}}],"responses":{"200":{"description":"The cached summary.","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Summary"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/api/v1/subscribe":{"post":{"summary":"Start a double-opt-in email subscription","tags":["subscribe"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeBody"}}}},"responses":{"202":{"description":"Confirmation email queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeResult"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/subscribe/confirm/{token}":{"get":{"summary":"Confirm a subscription (records CASL express consent)","tags":["subscribe"],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"HTML confirmation page."}}}},"/api/v1/subscribe/push":{"post":{"summary":"Web push opt-in (WS7) — no double opt-in loop","tags":["subscribe"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushSubscribeBody"}}}},"responses":{"201":{"description":"Push subscription active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeResult"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/watch":{"post":{"summary":"Occasion alert signup (WS7) — venue/category/keyword watch rule","tags":["subscribe"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchRuleBody"}}}},"responses":{"201":{"description":"Alert active or pending confirmation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeResult"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/u/{token}":{"get":{"summary":"One-click unsubscribe","tags":["subscribe"],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"HTML unsubscribe page."}}}},"/out/{eventId}":{"get":{"summary":"Tracked booking-link redirect","tags":["events"],"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to ticket URL."},"404":{"description":"No booking link."}}}},"/feeds/{area}/events.ics":{"get":{"summary":"iCalendar feed for an area","tags":["feeds"],"parameters":[{"name":"area","in":"path","required":true,"schema":{"type":"string"}},{"name":"category","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"text/calendar feed."}}}},"/feeds/{area}/events.rss":{"get":{"summary":"RSS feed for an area","tags":["feeds"],"parameters":[{"name":"area","in":"path","required":true,"schema":{"type":"string"}},{"name":"category","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"application/rss+xml feed."}}}},"/mcp":{"post":{"summary":"MCP endpoint (Streamable HTTP transport)","tags":["mcp"],"description":"Model Context Protocol server. Tools: whats_on, search_events, get_event, get_venue, plan_visit, subscribe.","responses":{"200":{"description":"JSON-RPC / SSE response."}}}},"/healthz":{"get":{"summary":"Liveness + DB check","tags":["ops"],"responses":{"200":{"description":"Healthy."},"503":{"description":"Degraded."}}}}}}