{
  "openapi": "3.1.0",
  "info": {
    "title": "Josh Burke NYC Events API",
    "version": "1.0.0",
    "description": "Anonymous, read-only NYC event discovery. Relative periods are convenience macros for an inclusive date range in America/New_York. Keep personal user context local and translate it into structured filters."
  },
  "servers": [
    {
      "url": "https://joshburke-events-api.nebula-monitor.workers.dev"
    }
  ],
  "paths": {
    "/api/v1/events": {
      "get": {
        "operationId": "findEvents",
        "summary": "Find current NYC events",
        "description": "Defaults to upcoming events tonight in America/New_York. Use date for one day or start_date/end_date for an inclusive range; period presets resolve to the same range model.",
        "parameters": [
          { "$ref": "#/components/parameters/Period" },
          { "$ref": "#/components/parameters/Date" },
          { "$ref": "#/components/parameters/StartDate" },
          { "$ref": "#/components/parameters/EndDate" },
          { "$ref": "#/components/parameters/After" },
          { "$ref": "#/components/parameters/Before" },
          { "$ref": "#/components/parameters/Borough" },
          { "$ref": "#/components/parameters/Category" },
          { "$ref": "#/components/parameters/Source" },
          { "$ref": "#/components/parameters/FreeOnly" },
          { "$ref": "#/components/parameters/MaxPrice" },
          { "$ref": "#/components/parameters/Query" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "Matching events and the server's query interpretation.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EventsResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/UpstreamError" }
        }
      }
    },
    "/api/v1/day/{date}": {
      "get": {
        "operationId": "findEventsByDay",
        "summary": "Find NYC events on one local date without a query string",
        "description": "Path-based single-day feed. Same envelope as /api/v1/events. Optional filters in the query string are still applied.",
        "parameters": [
          {
            "name": "date",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "date" }
          },
          { "$ref": "#/components/parameters/After" },
          { "$ref": "#/components/parameters/Before" },
          { "$ref": "#/components/parameters/Borough" },
          { "$ref": "#/components/parameters/Category" },
          { "$ref": "#/components/parameters/Source" },
          { "$ref": "#/components/parameters/FreeOnly" },
          { "$ref": "#/components/parameters/MaxPrice" },
          { "$ref": "#/components/parameters/Query" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "Matching events for that local start date.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EventsResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/UpstreamError" }
        }
      }
    },
    "/api/v1/vocabularies": {
      "get": {
        "operationId": "getEventVocabularies",
        "summary": "List allowed borough, category, and source values with tonight's counts",
        "responses": {
          "200": {
            "description": "Enumerations and how many tonight events currently use each value.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Vocabularies" }
              }
            }
          }
        }
      }
    },
    "/api/v1/events/{id}": {
      "get": {
        "operationId": "getEvent",
        "summary": "Get full public details for one event",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "maxLength": 128 }
          }
        ],
        "responses": {
          "200": {
            "description": "Full event details.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["generated_at", "timezone", "event"],
                  "properties": {
                    "generated_at": { "type": "string", "format": "date-time" },
                    "timezone": { "const": "America/New_York" },
                    "event": { "$ref": "#/components/schemas/Event" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/UpstreamError" }
        }
      }
    },
    "/api/v1/status": {
      "get": {
        "operationId": "getEventsStatus",
        "summary": "Get snapshot freshness and source health",
        "responses": {
          "200": {
            "description": "Current event snapshot status.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Status" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Period": {
        "name": "period",
        "in": "query",
        "description": "Convenience macro that resolves to start_date and end_date. Omit it when supplying explicit dates.",
        "schema": {
          "type": "string",
          "enum": ["tonight", "tomorrow", "week", "weekend", "custom"],
          "default": "tonight"
        }
      },
      "Date": {
        "name": "date",
        "in": "query",
        "description": "Single-day shortcut; equivalent to setting start_date and end_date to the same value.",
        "schema": { "type": "string", "format": "date" }
      },
      "StartDate": {
        "name": "start_date",
        "in": "query",
        "description": "Inclusive range start. Automatically selects custom range mode when period is omitted.",
        "schema": { "type": "string", "format": "date" }
      },
      "EndDate": {
        "name": "end_date",
        "in": "query",
        "description": "Inclusive range end; defaults to start_date. Maximum inclusive range is 31 days.",
        "schema": { "type": "string", "format": "date" }
      },
      "After": {
        "name": "after",
        "in": "query",
        "description": "Earliest local start time in HH:mm.",
        "schema": { "type": "string", "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$" }
      },
      "Before": {
        "name": "before",
        "in": "query",
        "description": "Latest local start time in HH:mm.",
        "schema": { "type": "string", "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$" }
      },
      "Borough": {
        "name": "borough",
        "in": "query",
        "description": "Comma-separated or repeated borough filters.",
        "schema": {
          "type": "string",
          "enum": ["manhattan", "brooklyn", "queens", "bronx", "staten-island", "other"]
        }
      },
      "Category": {
        "name": "category",
        "in": "query",
        "description": "Comma-separated or repeated category filters.",
        "schema": {
          "type": "string",
          "enum": ["music", "theater", "comedy", "art", "film", "dance", "community", "action", "singles", "literary", "market", "party", "workshop", "weird", "other"]
        }
      },
      "Source": {
        "name": "source",
        "in": "query",
        "description": "Comma-separated or repeated source filters.",
        "schema": {
          "type": "string",
          "enum": ["calred", "nycnoise", "skint", "nonsense", "luma", "comedybureau", "artforum", "ohmyrockness", "brooklynvegan"]
        }
      },
      "FreeOnly": {
        "name": "free_only",
        "in": "query",
        "schema": { "type": "boolean", "default": false }
      },
      "MaxPrice": {
        "name": "max_price",
        "in": "query",
        "schema": { "type": "number", "minimum": 0 }
      },
      "Query": {
        "name": "query",
        "in": "query",
        "description": "Event keyword only; do not send private user context.",
        "schema": { "type": "string", "maxLength": 100 }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 100 }
      }
    },
    "schemas": {
      "SourceRef": {
        "type": "object",
        "required": ["name", "url"],
        "properties": {
          "name": {
            "type": "string",
            "enum": ["calred", "nycnoise", "skint", "nonsense", "luma", "comedybureau", "artforum", "ohmyrockness", "brooklynvegan"]
          },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "Price": {
        "type": "object",
        "properties": {
          "min": { "type": "number" },
          "max": { "type": "number" },
          "text": { "type": "string" },
          "free": { "type": "boolean" }
        }
      },
      "Event": {
        "type": "object",
        "required": ["id", "title", "starts_at", "time_unknown", "categories", "api_url", "sources", "quality_score", "first_seen_at", "last_seen_at"],
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "summary": { "type": "string" },
          "description": { "type": "string" },
          "starts_at": { "type": "string", "format": "date-time" },
          "ends_at": { "type": "string", "format": "date-time" },
          "time_unknown": { "type": "boolean" },
          "venue": { "type": "string" },
          "address": { "type": "string" },
          "borough": {
            "type": "string",
            "enum": ["manhattan", "brooklyn", "queens", "bronx", "staten-island", "other"]
          },
          "price": { "$ref": "#/components/schemas/Price" },
          "categories": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["music", "theater", "comedy", "art", "film", "dance", "community", "action", "singles", "literary", "market", "party", "workshop", "weird", "other"]
            }
          },
          "url": { "type": "string", "format": "uri" },
          "api_url": { "type": "string", "format": "uri" },
          "sources": { "type": "array", "items": { "$ref": "#/components/schemas/SourceRef" } },
          "quality_score": { "type": "integer", "minimum": 0, "maximum": 100 },
          "first_seen_at": { "type": "string", "format": "date-time" },
          "last_seen_at": { "type": "string", "format": "date-time" }
        }
      },
      "EventsResponse": {
        "type": "object",
        "required": ["generated_at", "timezone", "interpreted_as", "total", "returned", "truncated", "events", "source_health"],
        "properties": {
          "generated_at": { "type": "string", "format": "date-time" },
          "timezone": { "const": "America/New_York" },
          "interpreted_as": {
            "type": "object",
            "required": ["period", "start_date", "end_date", "boroughs", "categories", "sources", "free_only", "limit", "used_defaults"],
            "properties": {
              "period": { "type": "string", "enum": ["tonight", "tomorrow", "week", "weekend", "custom"] },
              "start_date": { "type": "string", "format": "date" },
              "end_date": { "type": "string", "format": "date" },
              "used_defaults": { "type": "boolean" }
            },
            "additionalProperties": true
          },
          "total": { "type": "integer" },
          "returned": { "type": "integer" },
          "truncated": { "type": "boolean" },
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } },
          "source_health": { "type": "array", "items": { "$ref": "#/components/schemas/SourceHealth" } }
        }
      },
      "Status": {
        "type": "object",
        "required": ["ok", "generated_at", "timezone", "event_count", "source_health", "stale_source_count", "source_alerts", "usage"],
        "properties": {
          "ok": { "type": "boolean" },
          "generated_at": { "type": "string", "format": "date-time" },
          "timezone": { "const": "America/New_York" },
          "event_count": { "type": "integer" },
          "source_health": { "type": "array", "items": { "$ref": "#/components/schemas/SourceHealth" } },
          "stale_source_count": { "type": "integer" },
          "source_alerts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["name", "hours_since_success", "expected_cadence_hours", "message"],
              "properties": {
                "name": { "type": "string" },
                "hours_since_success": { "type": ["integer", "null"] },
                "expected_cadence_hours": { "type": "integer" },
                "message": { "type": "string" }
              }
            }
          },
          "usage": {
            "type": "object",
            "required": ["available", "utc_date", "estimated_requests", "sample_rate", "alert_threshold", "alert"],
            "properties": {
              "available": { "type": "boolean" },
              "utc_date": { "type": "string", "format": "date" },
              "estimated_requests": { "type": ["integer", "null"] },
              "sample_rate": { "type": "number", "minimum": 0, "maximum": 1 },
              "alert_threshold": { "type": "integer" },
              "alert": { "type": "boolean" }
            }
          }
        }
      },
      "SourceHealth": {
        "type": "object",
        "required": ["name", "lastSuccessAt", "lastEventCount", "ok", "expected_cadence_hours", "hours_since_success", "stale"],
        "properties": {
          "name": {
            "type": "string",
            "enum": ["calred", "nycnoise", "skint", "nonsense", "luma", "comedybureau", "artforum", "ohmyrockness", "brooklynvegan"]
          },
          "lastSuccessAt": { "type": ["string", "null"], "format": "date-time" },
          "lastEventCount": { "type": "integer" },
          "ok": { "type": "boolean" },
          "expected_cadence_hours": { "type": "integer" },
          "hours_since_success": { "type": ["integer", "null"] },
          "stale": { "type": "boolean" }
        }
      },
      "Vocabularies": {
        "type": "object",
        "required": ["generated_at", "timezone", "counted_as", "boroughs", "categories", "sources"],
        "properties": {
          "generated_at": { "type": "string", "format": "date-time" },
          "timezone": { "const": "America/New_York" },
          "counted_as": { "type": "object", "additionalProperties": true },
          "boroughs": { "type": "array", "items": { "$ref": "#/components/schemas/VocabularyCount" } },
          "categories": { "type": "array", "items": { "$ref": "#/components/schemas/VocabularyCount" } },
          "sources": { "type": "array", "items": { "$ref": "#/components/schemas/VocabularyCount" } }
        }
      },
      "VocabularyCount": {
        "type": "object",
        "required": ["value", "count"],
        "properties": {
          "value": { "type": "string" },
          "count": { "type": "integer" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" }
            }
          }
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "Invalid or unsupported query input.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Event not found.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Rate limit exceeded; see Retry-After.",
        "headers": {
          "Retry-After": { "schema": { "type": "integer" } }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "UpstreamError": {
        "description": "The source snapshot is temporarily unavailable.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  },
  "externalDocs": {
    "url": "https://joshburke.xyz/docs/events-api.md"
  }
}
