{
  "openapi": "3.1.0",
  "info": {
    "title": "DOW Hot Venues API",
    "description": "Where locals actually eat and drink — not where tourists go. DOW ranks independent restaurants, pubs and bars at neighbourhood level using native-language review velocity. Monthly updates. No chains, no paid placements.\n\nEach venue includes a rich editorial profile (quick_verdict, book_if, skip_if, best_for tags, strengths, signature dishes) plus reviewer quotes. Use the profile fields to make natural recommendations — lead with the verdict, not the numbers.\n\nZone-level responses include use-case scenarios for intent matching.\n\nCovered cities (use these exact names in the city parameter):\n\nUK (8): manchester, edinburgh, glasgow, birmingham, belfast, leeds, liverpool, bath\nPortugal (13): porto, lisbon, braga, guimaraes, coimbra, aveiro, evora, cascais, ericeira, setubal, lagos, margem sul, tavira\nSpain (13): seville, bilbao, vigo, burgos, san sebastian, santiago de compostela, santander, oviedo, valencia, granada, cadiz, marbella, girona\n\nNOT covered: London, Madrid, Barcelona — every other guide covers those. DOW focuses on cities that don't get attention.\n\nEach city has 1-14 walkable zones. Pass the zone parameter for neighbourhood-level results (e.g. zone=triana for Seville's Triana, zone=northern-quarter for Manchester's NQ, zone=ribeira for Porto's Ribeira, zone=puerto-banus for Marbella's Puerto Banús).\n\nAuthentication: send your API key in the x-api-key header.\n\nRate limits: /api/hot-venues allows 60 requests per client IP per 60-second window. Responses carry RateLimit-Policy, RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window resets). A request over the limit gets HTTP 429 with Retry-After (seconds). The counter is held in server memory, so treat the figures as a guide for pacing, not a billing meter.\n\nErrors: every 4xx and 5xx response is RFC 9457 application/problem+json with type, title, status, detail and a machine-readable code (invalid_api_key, rate_limited, invalid_zone, internal_error). A legacy error member repeats detail for older clients.\n\nVersioning and deprecation policy: the API is versioned by the API-Version response header (currently 2.1.0, equal to info.version); the URL path carries no version. Additive changes (new fields, new optional parameters) are not breaking and do not change the major number. A breaking change raises the major number. Before a field, parameter or endpoint is removed it is announced with a Deprecation response header and, once a removal date is set, a Sunset header. The intended notice period is at least 90 days.",
    "version": "2.1.0",
    "contact": {
      "name": "DondeOndeWhere",
      "url": "https://www.donde-onde-where.com"
    }
  },
  "servers": [
    {
      "url": "https://www.donde-onde-where.com",
      "description": "Production"
    }
  ],
  "components": {
    "headers": {
      "API-Version": {
        "description": "API version that produced this response (equal to info.version). Sent on every response, including errors.",
        "schema": { "type": "string", "example": "2.1.0" }
      },
      "RateLimit-Policy": {
        "description": "The quota policy in force: requests per window in seconds, e.g. 60;w=60.",
        "schema": { "type": "string", "example": "60;w=60" }
      },
      "RateLimit-Limit": {
        "description": "Requests allowed per window.",
        "schema": { "type": "integer", "example": 60 }
      },
      "RateLimit-Remaining": {
        "description": "Requests left in the current window.",
        "schema": { "type": "integer", "minimum": 0 }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the current window resets.",
        "schema": { "type": "integer", "minimum": 1 }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying. Sent on 429 only.",
        "schema": { "type": "integer", "minimum": 1 }
      },
      "WWW-Authenticate": {
        "description": "API-key challenge. Sent on 401 only: ApiKey realm=\"hot-venues\", header=\"x-api-key\".",
        "schema": { "type": "string" }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details, served as application/problem+json.",
        "required": ["type", "title", "status", "detail", "code"],
        "properties": {
          "type": { "type": "string", "description": "Problem type identifier (a URN, not a link).", "example": "urn:problem-type:donde-onde-where:rate_limited" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "detail": { "type": "string" },
          "code": { "type": "string", "description": "Machine-readable error code.", "enum": ["invalid_api_key", "rate_limited", "invalid_zone", "internal_error"] },
          "error": { "type": "string", "deprecated": true, "description": "Legacy member kept for older clients. Same text as detail." }
        }
      },
      "VenueProfile": {
        "type": "object",
        "nullable": true,
        "description": "Rich editorial content for natural recommendations. Lead with quick_verdict, match intent with best_for tags.",
        "properties": {
          "quick_verdict": { "type": "string", "description": "30-second summary — use this as the lead line when recommending this venue" },
          "book_if": { "type": "string", "nullable": true, "description": "When this venue is the right choice — use to match user intent" },
          "skip_if": { "type": "string", "nullable": true, "description": "Honest caveat — mention when relevant to the user's situation" },
          "best_for": { "type": "array", "items": { "type": "string" }, "description": "Tags like 'Date night', 'Groups', 'Late night', 'Tapas' — match against what the user is looking for" },
          "strengths": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "evidence": { "type": "string" } } }, "description": "Top strengths with evidence from reviews" },
          "signature_dishes": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "price": { "type": "number" }, "description": { "type": "string" } } }, "description": "Notable dishes or drinks with prices" },
          "estimated_cost_pp": { "type": "number", "nullable": true, "description": "Estimated cost per person in local currency (EUR or GBP)" },
          "reservation_policy": { "type": "string", "nullable": true, "enum": ["required", "recommended", "recommended_weekends", "walk_in", "no_reservations"], "description": "Whether booking is needed" },
          "late_night_until": { "type": "string", "nullable": true, "description": "Latest closing time if open after 23:00 (e.g. '02:00')" },
          "hours_today": { "type": "object", "nullable": true, "properties": { "open": { "type": "string" }, "close": { "type": "string" } }, "description": "Today's opening hours only" }
        }
      },
      "VenueQuote": {
        "type": "object",
        "properties": {
          "quote_text": { "type": "string", "description": "Actual reviewer quote — use to add personality to recommendations" },
          "quote_type": { "type": "string", "enum": ["food", "service", "atmosphere", "overall", "experience"], "description": "What aspect of the venue the quote covers" }
        }
      },
      "ZonePick": {
        "type": "object",
        "nullable": true,
        "description": "A curated pick for a specific use case",
        "properties": {
          "place_id": { "type": "string" },
          "name": { "type": "string" },
          "reason": { "type": "string" },
          "venue_type": { "type": "string" },
          "hot_list_rank": { "type": "integer" }
        }
      },
      "ZoneContext": {
        "type": "object",
        "nullable": true,
        "description": "Editorial context for the zone — use intro for scene-setting. For intent questions ('best date night', 'best value'), match against each venue's profile.best_for and profile.book_if.",
        "properties": {
          "intro": { "type": "string", "nullable": true, "description": "Zone introduction — use to set the scene when describing a neighbourhood" },
          "local_tips": { "type": "string", "nullable": true, "description": "Insider tips from locals" },
          "known_for": { "type": "array", "items": { "type": "string" }, "description": "What this zone is famous for" },
          "scenarios": { "type": "array", "items": { "type": "object" }, "description": "Use-case scenarios with persona, situation, and recommended venue" }
        }
      }
    },
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key"
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/api/hot-venues": {
      "get": {
        "operationId": "getHotVenues",
        "summary": "Independent restaurants, pubs and bars ranked by recent written review activity, not star ratings",
        "description": "Ranked venues by Hot Score (0-100) with rich editorial profiles and reviewer quotes. Use profile.quick_verdict for recommendations, and profile.best_for and profile.book_if for intent matching and 'best X' questions.",
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "City name (e.g. porto, manchester, seville, bilbao, edinburgh). Lowercase.",
            "schema": { "type": "string" }
          },
          {
            "name": "zone",
            "in": "query",
            "required": false,
            "description": "Zone slug for neighbourhood-level results (e.g. ribeira, northern-quarter, triana, ensanche). Lowercase with hyphens.",
            "schema": { "type": "string" }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Filter by venue type",
            "schema": { "type": "string", "enum": ["all", "restaurant", "bar"], "default": "all" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of venues to return (1-50)",
            "schema": { "type": "integer", "default": 10, "minimum": 1, "maximum": 50 }
          }
        ],
        "responses": {
          "401": {
          "description": "Missing or invalid x-api-key.",
          "headers": {
            "API-Version": { "$ref": "#/components/headers/API-Version" },
            "WWW-Authenticate": { "$ref": "#/components/headers/WWW-Authenticate" }
          },
          "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
        },
        "429": {
          "description": "Rate limit exceeded (60 requests per client IP per 60 seconds).",
          "headers": {
            "API-Version": { "$ref": "#/components/headers/API-Version" },
            "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" },
            "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" },
            "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" },
            "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" },
            "Retry-After": { "$ref": "#/components/headers/Retry-After" }
          },
          "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
        },
        "500": {
          "description": "Unexpected server error.",
          "headers": { "API-Version": { "$ref": "#/components/headers/API-Version" } },
          "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
        },
        "200": {
          "headers": {
            "API-Version": { "$ref": "#/components/headers/API-Version" },
            "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" },
            "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" },
            "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" },
            "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" }
          },
            "description": "Ranked venue list with editorial profiles and zone context",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "object",
                      "description": "Echo of query parameters used"
                    },
                    "zone": { "type": "string", "nullable": true },
                    "city": { "type": "string", "nullable": true },
                    "generated_at": { "type": "string", "format": "date-time" },
                    "venue_count": { "type": "integer" },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "surging": { "type": "integer", "description": "Venues with explosive recent growth" },
                        "hot": { "type": "integer", "description": "Number of venues trending hot" },
                        "rising": { "type": "integer", "description": "Number of rising venues" },
                        "cooling": { "type": "integer", "description": "Venues past their peak" },
                        "avg_local_language_pct": { "type": "integer", "description": "Average percentage of reviews in local language — higher means more locals" },
                        "description": { "type": "string", "description": "Human-readable summary of the zone/city scene" }
                      }
                    },
                    "venues": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "place_id": { "type": "string" },
                          "name": { "type": "string" },
                          "type": { "type": "string", "description": "Raw GBP primary category as the venue owner selected it (e.g. 'Restaurant', 'Cocktail bar', 'Tapas bar', 'Italian restaurant', 'Pub', 'Restaurante', 'Tasca'). Country-specific terms appear verbatim. Some legacy rows still carry lowercase bucket values ('restaurant', 'bar', 'gastropub')." },
                          "cuisine": { "type": "string", "nullable": true },
                          "price_band": { "type": "string", "nullable": true, "description": "Price in local currency symbols (e.g. ££, €€€)" },
                          "rating": { "type": "number" },
                          "review_count": { "type": "integer" },
                          "hot_score": { "type": "number", "description": "0-100 composite score — do NOT show this number to users, use momentum tag instead" },
                          "momentum": { "type": "string", "enum": ["surging", "hot", "rising", "steady", "cooling", "new"], "description": "Velocity trend — translate naturally: surging='blowing up right now', hot='consistently packed', rising='gaining real traction', steady='reliably good', cooling='quieter lately'" },
                          "zone": { "type": "string" },
                          "city": { "type": "string" },
                          "velocity": {
                            "type": "object",
                            "properties": {
                              "text_reviews_90d": { "type": "integer", "description": "Written reviews in last 90 days — use for context, not display" },
                              "text_reviews_30d": { "type": "integer", "description": "Written reviews in last 30 days" }
                            }
                          },
                          "local_signal": {
                            "type": "object",
                            "properties": {
                              "local_language_pct": { "type": "integer", "description": "Percentage of reviews in native language — do NOT show as a number" },
                              "strength": { "type": "string", "enum": ["strong", "moderate", "tourist-heavy"], "description": "Translate naturally: strong='a genuine locals spot', moderate='good mix of locals and visitors', tourist-heavy='more popular with visitors'" }
                            }
                          },
                          "featured_quote": { "type": "string", "nullable": true, "description": "Best review quote — use to add personality" },
                          "top_quotes": { "type": "array", "items": { "$ref": "#/components/schemas/VenueQuote" }, "description": "Top 2-3 reviewer quotes for this venue" },
                          "profile": { "$ref": "#/components/schemas/VenueProfile" },
                          "booking_url": { "type": "string", "nullable": true, "description": "Direct booking link — include when recommending" },
                          "dow_url": { "type": "string", "description": "Full DOW profile page — always include as 'See the full profile' link" }
                        }
                      }
                    },
                    "zone_context": { "$ref": "#/components/schemas/ZoneContext" },
                    "methodology": {
                      "type": "object",
                      "description": "Scoring methodology for transparency — reference when users ask how rankings work"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
