{
  "openapi": "3.1.0",
  "info": {
    "title": "BuscaCerca — API pública",
    "version": "1.102.1",
    "summary": "El buscador de los negocios de Mercedes (Soriano, Uruguay).",
    "description": "Superficie **pública y de solo lectura** de BuscaCerca: negocios, servicios, profesionales y emprendimientos de Mercedes, con sus categorías, horarios y canales de contacto.\n\nEsto describe lo que puede consultar cualquiera, sin credenciales. La API de gestión (`/api/admin/*`, `/api/comerciante/*`, `/api/monetizacion/*`) exige JWT, es privada y **no se documenta acá** a propósito: no es una superficie de integración.\n\n### Versiones\n\nEl contrato estable es `/api/v1/...`. Un cambio incompatible sale como una versión nueva del path (`/api/v2`), y **la anterior sigue funcionando al menos 6 meses**.\n\n`/api/...` sin versión también funciona y apunta siempre a la versión vigente: lo usa el frontend, que se deploya junto con el backend. **Para integrar de afuera usá la URL con versión** — la sin versión puede cambiar con la app.\n\n### Deprecación\n\nUn endpoint en camino de salida responde con `Deprecation` (RFC 9745) con la fecha en que se marcó, `Sunset` (RFC 8594) con la fecha en que deja de responder, y un `Link rel=\"deprecation\"` a la explicación. Hoy no hay nada deprecado.\n\n### Límites\n\n30 pedidos por minuto por IP. Pasado ese techo la respuesta es `503` con `code: RATE_LIMITED` — esperá un minuto y reintentá de a un pedido por vez, no en ráfaga.\n\n### Errores\n\nTodos los errores son JSON con la misma forma (`Error` más abajo): `code` es estable y sirve para ramificar, `hint` dice qué hacer. Nunca sale HTML.\n\n### Descubrimiento\n\n- Índice de la API: https://buscacerca.uy/api\n- Catálogo estándar (RFC 9727): https://buscacerca.uy/.well-known/api-catalog\n- Documentación para personas: https://buscacerca.uy/developers\n- Páginas públicas: https://buscacerca.uy/api/sitemap.xml\n- Reglas de rastreo, con permiso explícito para los agentes de IA: https://buscacerca.uy/robots.txt\n\nLas páginas del sitio también se sirven en Markdown con `Accept: text/markdown` (acceptmarkdown.com).",
    "contact": {
      "name": "BuscaCerca",
      "url": "https://buscacerca.uy",
      "email": "hola@buscacerca.uy"
    }
  },
  "servers": [
    {
      "url": "https://buscacerca.uy/api/v1",
      "description": "Producción — el contrato estable"
    },
    {
      "url": "https://buscacerca.uy/api",
      "description": "Alias sin versión: apunta siempre a la versión vigente. Lo usa el frontend; para integrar de afuera, usá el de arriba."
    }
  ],
  "tags": [
    {
      "name": "Negocios",
      "description": "Fichas de negocios publicados."
    },
    {
      "name": "Categorías",
      "description": "El árbol de categorías del catálogo."
    },
    {
      "name": "Lugares",
      "description": "Países y ciudades cubiertas."
    }
  ],
  "paths": {
    "/locales": {
      "get": {
        "tags": [
          "Negocios"
        ],
        "summary": "Buscar negocios",
        "description": "Devuelve negocios publicados de una ciudad. Con `busqueda` usa el buscador semántico (embeddings + reglas), que entiende lo que la persona necesita y no solo las palabras que escribió: \"algo para el dolor de muelas\" trae odontólogos y farmacias.\n\nSin `busqueda` devuelve el listado, filtrable por categoría y atributos.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Pais"
          },
          {
            "$ref": "#/components/parameters/Ciudad"
          },
          {
            "name": "busqueda",
            "in": "query",
            "description": "Lo que se busca, en lenguaje natural.",
            "schema": {
              "type": "string"
            },
            "example": "donde arreglo una rueda"
          },
          {
            "name": "categoriaSlug",
            "in": "query",
            "description": "Filtra por categoría. Un slug inexistente devuelve una lista vacía, no el catálogo entero.",
            "schema": {
              "type": "string"
            },
            "example": "pizzerias"
          },
          {
            "name": "categoriaId",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "parentCategoriaId",
            "in": "query",
            "description": "Incluye la categoría y todas sus hijas.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "reparto",
            "in": "query",
            "description": "Solo los que hacen reparto a domicilio.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "abierto24hs",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "promo",
            "in": "query",
            "description": "Solo los que tienen una promoción vigente.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "sinGluten",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "relevancia",
                "nombre",
                "reciente"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Los negocios que coinciden.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaDeLocales"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locales/{idOrSlug}": {
      "get": {
        "tags": [
          "Negocios"
        ],
        "summary": "Una ficha de negocio",
        "description": "Acepta el slug de la URL pública (`/uy/mercedes/<slug>`) o el id numérico.",
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "aca-toys-mercedes"
          }
        ],
        "responses": {
          "200": {
            "description": "La ficha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Local"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "503": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locales/{slug}/relacionados": {
      "get": {
        "tags": [
          "Negocios"
        ],
        "summary": "Negocios parecidos",
        "description": "Otros negocios de las mismas categorías, en la misma ciudad.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Los relacionados.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Local"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/locales/mapa": {
      "get": {
        "tags": [
          "Negocios"
        ],
        "summary": "Coordenadas para el mapa",
        "description": "Versión liviana del listado: solo lo que hace falta para poner un pin.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Pais"
          },
          {
            "$ref": "#/components/parameters/Ciudad"
          },
          {
            "name": "categoriaSlug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Los negocios con coordenadas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PuntoEnMapa"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/categorias": {
      "get": {
        "tags": [
          "Categorías"
        ],
        "summary": "Categorías del catálogo",
        "parameters": [
          {
            "name": "conConteo",
            "in": "query",
            "description": "Agrega cuántos negocios publicados tiene cada una.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "$ref": "#/components/parameters/Pais"
          },
          {
            "$ref": "#/components/parameters/Ciudad"
          }
        ],
        "responses": {
          "200": {
            "description": "Las categorías.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Categoria"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/categorias/arbol": {
      "get": {
        "tags": [
          "Categorías"
        ],
        "summary": "El árbol completo",
        "description": "Categorías raíz con sus subcategorías anidadas.",
        "responses": {
          "200": {
            "description": "El árbol.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Categoria"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/directorio": {
      "get": {
        "tags": [
          "Categorías"
        ],
        "summary": "Índice de una ciudad",
        "description": "Las categorías con negocios publicados, agrupadas — es lo que sirve la página `/directorio`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Pais"
          },
          {
            "$ref": "#/components/parameters/Ciudad"
          }
        ],
        "responses": {
          "200": {
            "description": "El índice.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/ciudades": {
      "get": {
        "tags": [
          "Lugares"
        ],
        "summary": "Ciudades cubiertas",
        "description": "Hoy solo Mercedes (Soriano, Uruguay). La expansión es por cercanía, no nacional.",
        "responses": {
          "200": {
            "description": "Las ciudades.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Ciudad"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "tags": [
          "Lugares"
        ],
        "summary": "Todas las páginas públicas",
        "description": "XML estándar de sitemaps. Es la lista completa y autoritativa de URLs rastreables: la portada, `/directorio`, las páginas de categoría y la ficha de cada negocio publicado.",
        "responses": {
          "200": {
            "description": "El sitemap.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Pais": {
        "name": "pais",
        "in": "query",
        "description": "Slug del país.",
        "schema": {
          "type": "string",
          "default": "uy"
        }
      },
      "Ciudad": {
        "name": "ciudad",
        "in": "query",
        "description": "Slug de la ciudad.",
        "schema": {
          "type": "string",
          "default": "mercedes"
        }
      }
    },
    "responses": {
      "NoEncontrado": {
        "description": "No existe.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Local no encontrado",
              "code": "NOT_FOUND",
              "message": "Local no encontrado",
              "hint": "Verificá la URL y el identificador. El catálogo público se lista en https://buscacerca.uy/api/sitemap.xml.",
              "status": 404
            }
          }
        }
      },
      "RateLimited": {
        "description": "Demasiados pedidos desde esta IP.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Demasiadas solicitudes.",
              "code": "RATE_LIMITED",
              "message": "Demasiadas solicitudes desde esta IP.",
              "hint": "Hay un tope de 30 pedidos por minuto por IP en la API abierta. Esperar un minuto y reintentar de a uno por vez.",
              "status": 503
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "La forma de TODOS los errores de esta API. Nunca sale HTML.",
        "required": [
          "error",
          "code",
          "message",
          "hint",
          "status"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "El mensaje, tal cual. Existe desde antes que el resto del sobre y se mantiene por compatibilidad."
          },
          "code": {
            "type": "string",
            "description": "Código estable, para ramificar sin leer el texto.",
            "enum": [
              "BAD_REQUEST",
              "UNAUTHORIZED",
              "FORBIDDEN",
              "NOT_FOUND",
              "ENDPOINT_NOT_FOUND",
              "CONFLICT",
              "GONE",
              "PAYLOAD_TOO_LARGE",
              "UNPROCESSABLE_ENTITY",
              "RATE_LIMITED",
              "INTERNAL_ERROR",
              "BAD_GATEWAY",
              "SERVICE_UNAVAILABLE",
              "GATEWAY_TIMEOUT"
            ]
          },
          "message": {
            "type": "string",
            "description": "Lo mismo que `error`, legible."
          },
          "hint": {
            "type": "string",
            "description": "Qué hacer al respecto: si conviene reintentar, qué corregir, dónde mirar."
          },
          "status": {
            "type": "integer",
            "description": "El status HTTP, repetido en el cuerpo."
          },
          "detalles": {
            "type": "array",
            "description": "Presente en errores de validación: qué campo falló.",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "ListaDeLocales": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Local"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos coinciden en total."
          },
          "hasMore": {
            "type": "boolean",
            "description": "Si quedan más después de este `offset`."
          },
          "sugerencia": {
            "type": [
              "string",
              "null"
            ],
            "description": "Si la búsqueda no trajo nada, qué probar en su lugar."
          }
        }
      },
      "Local": {
        "type": "object",
        "description": "Un negocio, servicio, profesional o emprendimiento publicado.",
        "required": [
          "id",
          "nombre",
          "slug"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "nombre": {
            "type": "string",
            "examples": [
              "Aca Toys"
            ]
          },
          "slug": {
            "type": "string",
            "description": "El de la URL pública: `/uy/mercedes/<slug>`.",
            "examples": [
              "aca-toys-mercedes"
            ]
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ]
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "J. E. Rodo y Zapican"
            ]
          },
          "telefono": {
            "type": [
              "string",
              "null"
            ]
          },
          "whatsapp": {
            "type": [
              "string",
              "null"
            ]
          },
          "instagram": {
            "type": [
              "string",
              "null"
            ],
            "description": "El handle, sin la arroba."
          },
          "facebook": {
            "type": [
              "string",
              "null"
            ]
          },
          "webUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "fotoUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "lat": {
            "type": [
              "number",
              "null"
            ]
          },
          "lng": {
            "type": [
              "number",
              "null"
            ]
          },
          "haceReparto": {
            "type": "boolean",
            "description": "Si lleva a domicilio."
          },
          "abierto24hs": {
            "type": "boolean"
          },
          "horarios": {
            "type": "array",
            "description": "Un tramo por día de la semana.",
            "items": {
              "$ref": "#/components/schemas/Horario"
            }
          },
          "categorias": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Categoria"
            }
          }
        }
      },
      "Horario": {
        "type": "object",
        "properties": {
          "dia": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "description": "0 = domingo."
          },
          "abre": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "08:00"
            ]
          },
          "cierra": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "12:30"
            ]
          }
        }
      },
      "Categoria": {
        "type": "object",
        "required": [
          "id",
          "nombre"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "nombre": {
            "type": "string",
            "examples": [
              "Juguetería"
            ]
          },
          "slug": {
            "type": "string",
            "examples": [
              "jugueteria"
            ]
          },
          "parentId": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null en las categorías raíz."
          },
          "conteo": {
            "type": "integer",
            "description": "Cuántos negocios publicados tiene. Solo con `conConteo=true`."
          },
          "subcategorias": {
            "type": "array",
            "description": "Solo en `/categorias/arbol`.",
            "items": {
              "$ref": "#/components/schemas/Categoria"
            }
          }
        }
      },
      "Ciudad": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "nombre": {
            "type": "string",
            "examples": [
              "Mercedes"
            ]
          },
          "slug": {
            "type": "string",
            "examples": [
              "mercedes"
            ]
          },
          "pais": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              }
            }
          }
        }
      },
      "PuntoEnMapa": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "nombre": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          }
        }
      }
    }
  },
  "externalDocs": {
    "url": "https://buscacerca.uy/developers",
    "description": "Portal para desarrolladores y agentes"
  }
}
