{
 "openapi": "3.1.0",
 "info": {
  "title": "Salt Lake City Events data API",
  "version": "0.20.0",
  "summary": "Read-only Salt Lake City area and Wasatch Front events.",
  "description": "Public, read-only, no authentication. All rights reserved: an assistant may answer one person's question from it, crediting Salt Lake City Events (https://slc.whatsupin.ai) and linking each event to its original listing; bulk copying needs express written permission (admin@whatsupin.ai). Terms: https://slc.whatsupin.ai/terms/#data. An MCP server (Streamable HTTP, JSON-RPC) with tools search_events, list_upcoming, get_event, get_city and find_performer is at https://slc.whatsupin.ai/mcp (setup: https://slc.whatsupin.ai/agents/). Coverage and accuracy: https://slc.whatsupin.ai/coverage/. Changes: https://slc.whatsupin.ai/changelog/.",
  "license": {
   "name": "All rights reserved; no bulk copying without written permission",
   "url": "https://slc.whatsupin.ai/terms/#data"
  },
  "contact": {
   "email": "admin@whatsupin.ai"
  }
 },
 "servers": [
  {
   "url": "https://slc.whatsupin.ai"
  }
 ],
 "paths": {
  "/data/events.json": {
   "get": {
    "operationId": "getEvents",
    "summary": "Upcoming events, nearest weeks",
    "responses": {
     "200": {
      "description": "Events plus `rest_parts`, the paths of the later-events files.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "count": {
           "type": "integer"
          },
          "events": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Event"
           }
          },
          "rest": {
           "type": "string",
           "description": "Path of the first file holding later events (data/events_rest.json)."
          },
          "rest_parts": {
           "type": "array",
           "items": {
            "type": "string"
           },
           "description": "Paths of every later-events file, in order (data/events_rest.json, data/events_rest_2.json, ...)."
          },
          "rest_count": {
           "type": "integer"
          },
          "generated": {
           "type": "string",
           "format": "date-time",
           "description": "When this data was published."
          },
          "license": {
           "type": "object"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/data/events_rest.json": {
   "get": {
    "operationId": "getLaterEvents",
    "summary": "Events further out (the first part; events.json `rest_parts` lists any more)",
    "responses": {
     "200": {
      "description": "Same event shape.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "events": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Event"
           }
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/data/changes.json": {
   "get": {
    "operationId": "getChanges",
    "summary": "Upcoming events taken down in the last two weeks (cancelled, delisted or merged into another card), and when change tracking began",
    "responses": {
     "200": {
      "description": "`removed`: [{id, title, date, removed}], newest first; `tracked_since`; `generated`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/data/coverage.json": {
   "get": {
    "operationId": "getCoverage",
    "summary": "Counts, per-source freshness, latest validator numbers",
    "responses": {
     "200": {
      "description": "Coverage.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/status": {
   "get": {
    "operationId": "getStatus",
    "summary": "Is the service up, and how old is the data",
    "responses": {
     "200": {
      "description": "status ok, or degraded when the data is older than 48 hours.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/llms.txt": {
   "get": {
    "operationId": "getLlmsTxt",
    "summary": "Plain-text pointer for LLM clients",
    "responses": {
     "200": {
      "description": "text/plain"
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "Event": {
    "type": "object",
    "description": "One upcoming event in data/events.json (and data/events_rest.json). Fields marked optional are absent when unknown.",
    "required": [
     "id",
     "title",
     "date",
     "link"
    ],
    "properties": {
     "id": {
      "type": "string",
      "pattern": "^[0-9a-f]{12}$",
      "description": "Stable public id. Kept while the event stays listed: a retitle, reschedule or new link by the source does not change it. Ids are never reused for a different event."
     },
     "title": {
      "type": "string"
     },
     "date": {
      "type": "string",
      "format": "date",
      "description": "Local start date (site time zone)."
     },
     "start_time": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Local start with UTC offset; null for all-day or untimed."
     },
     "end_time": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "location": {
      "type": [
       "string",
       "null"
      ]
     },
     "venue_status": {
      "type": "string",
      "enum": [
       "rsvp",
       "tba",
       "hidden",
       "title",
       "text",
       "unset"
      ],
      "description": "Optional. Present only when location is empty, and says why (a venue is never guessed): rsvp (address shared after RSVP or registration), tba (venue not decided), hidden (the source does not show it publicly), title (the title names the place), text (the description names a place), unset (the host left it blank). Open the event's link for the venue."
     },
     "hood": {
      "type": "string",
      "description": "Optional. Home-city neighborhood of the venue (MCP calls this `neighborhood`)."
     },
     "ll": {
      "type": "array",
      "items": {
       "type": "number"
      },
      "minItems": 2,
      "maxItems": 2,
      "description": "[lat, lon] of the venue (optional)."
     },
     "price": {
      "type": [
       "string",
       "null"
      ],
      "description": "Only an all-in price or \"Free\". When the source does not state the all-in total there is no number; see tix."
     },
     "free": {
      "type": "boolean",
      "description": "Optional. True when the listing is free."
     },
     "tix": {
      "type": "boolean",
      "description": "Optional. Paid event; the source does not state an all-in price, so none is shown."
     },
     "categories": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "tags": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "topic/<name> tags and others."
     },
     "performers": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Optional."
     },
     "sold_out": {
      "type": "boolean",
      "description": "Optional. True when every posting we know of says tickets are gone (a status, not a price)."
     },
     "event_status": {
      "type": "string",
      "enum": [
       "sold_out",
       "postponed",
       "rescheduled",
       "moved"
      ],
      "description": "Optional. What a source says about the event: postponed (no new date yet; it stays on its original date), rescheduled (this is the new date), moved (to another venue; location is the new one), sold_out. Absent = scheduled as listed. Cancelled events are left out."
     },
     "status_note": {
      "type": "string",
      "description": "Optional. The source's own wording of event_status, e.g. \"Moved to The Warfield\" or \"Rescheduled from 8/8/26\"."
     },
     "vt": {
      "type": "string",
      "format": "date-time",
      "description": "Optional. Checked live: when we last opened the original listing and found it matching (start time, venue, not cancelled), in local time. Absent = not checked in the last two days."
     },
     "recurring": {
      "type": "string",
      "enum": [
       "weekly",
       "biweekly",
       "monthly"
      ],
      "description": "Optional. The event is one date of a repeating series (same title at the same venue)."
     },
     "nh": {
      "type": "boolean",
      "description": "Optional. True when the event's host (organizer, group or promoter) first appeared in our listings within the last two weeks. A plain 'new' marker, not a quality signal. Absent otherwise."
     },
     "link": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "The original listing. Link to it when you use the event."
     },
     "srcs": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Every source that lists the event (MCP calls this `sources`)."
     },
     "org": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Organizer per source, aligned with srcs (optional)."
     },
     "fs": {
      "type": "string",
      "format": "date",
      "description": "first_seen: first day a fetch of the source listed this event."
     },
     "ls": {
      "type": "string",
      "format": "date",
      "description": "last_seen: latest day a fetch of the source listed it. Older than ft means the source has stopped listing it."
     },
     "ft": {
      "type": "string",
      "format": "date",
      "description": "last_fetched: the day the source was last fetched."
     },
     "ch": {
      "type": "string",
      "description": "changed: when an export last saw the title, date or time, venue, link, price, sold-out flag or status change (local ISO date-time to the minute). Events listed before change tracking began carry their first-seen day. The MCP's changed_since filters on it."
     },
     "img": {
      "type": "string",
      "format": "uri",
      "maxLength": 500,
      "description": "Optional, and only on sites that show source pictures. An https URL of the event's own promo picture as the source publishes it (we link to it, we do not host a copy). Show it with its credit img_by, load it lazily without a referrer, and fall back to your own art when it fails."
     },
     "img_by": {
      "type": "string",
      "description": "Optional, always with img. Credit label for the picture: the platform (\"Ticketmaster\", \"Eventbrite\", \"Luma\", \"Meetup\") or the host of the event's own listing. \"Meetup (group photo)\" means the organizing group's photo, used when the event has none of its own."
     },
     "flyer_url": {
      "type": "string",
      "format": "uri",
      "maxLength": 500,
      "description": "Optional. Original public Instagram or Bluesky post whose flyer supplied event facts. Link to the post; do not embed or copy its media. The event's primary link remains the ticket or listing page."
     },
     "flyer_by": {
      "type": "string",
      "enum": [
       "Instagram",
       "Bluesky"
      ],
      "description": "Optional, always with flyer_url. Platform label for the original flyer post."
     },
     "dup_count": {
      "type": "integer",
      "description": "Optional. Number of sources merged into this card."
     },
     "rv": {
      "type": "string",
      "description": "Provenance fingerprint (keyed by us). Keep it if you copy the record."
     }
    }
   }
  }
 }
}
