{
    "openapi": "3.1.0",
    "info": {
        "title": "LocalityBiz API",
        "description": "Search businesses by location and retrieve full business profiles. Paid B2B API with monthly quotas.",
        "version": "1.0.0",
        "contact": {
            "email": "info@localitybiz.com"
        }
    },
    "servers": [
        {
            "url": "https://localitybiz.com.ua/api/v1",
            "description": "Production"
        }
    ],
    "security": [
        {
            "bearerAuth": []
        },
        {
            "apiKeyQuery": []
        }
    ],
    "tags": [
        {
            "name": "Search",
            "description": "Geo-search across the LocalityBiz directory."
        },
        {
            "name": "Business",
            "description": "Full business profiles."
        },
        {
            "name": "Locations",
            "description": "Country / region / city directory used by Search."
        },
        {
            "name": "Categories",
            "description": "Two-level schema.org taxonomy used by Search."
        }
    ],
    "paths": {
        "/search": {
            "get": {
                "tags": [
                    "Search"
                ],
                "summary": "Search businesses in a region or a circle",
                "description": "Full-text search either inside a directory region/city (use `region`) or inside a (lat, lng, radius) circle. Exactly one of the two location modes is required. Sorted by relevance + rating. Counts against the monthly search quota.",
                "parameters": [
                    {
                        "name": "q",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "maxLength": 200
                        },
                        "description": "Search query. Required."
                    },
                    {
                        "name": "region",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Region or region+city code from /api/v1/locations. \"<regionId>\" (e.g. \"605\") narrows to the whole region; \"<regionId>_<cityId>\" (e.g. \"605_1322\") narrows to one city within. When present, lat/lng/radius_km are ignored."
                    },
                    {
                        "name": "lat",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "number",
                            "format": "float",
                            "minimum": -90,
                            "maximum": 90
                        },
                        "description": "Latitude of search centre. Required when `region` is omitted."
                    },
                    {
                        "name": "lng",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "number",
                            "format": "float",
                            "minimum": -180,
                            "maximum": 180
                        },
                        "description": "Longitude of search centre. Required when `region` is omitted."
                    },
                    {
                        "name": "radius_km",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "number",
                            "format": "float",
                            "minimum": 0.1,
                            "maximum": 50
                        },
                        "description": "Search radius in km. Required when `region` is omitted."
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "default": 1
                        },
                        "description": "Page number; 30 results per page."
                    },
                    {
                        "name": "category",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Filter by schema.org type code (e.g. \"Restaurant\", \"Dentist\")."
                    },
                    {
                        "name": "min_rating",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 5
                        },
                        "description": "Only return businesses with rating ≥ this."
                    },
                    {
                        "name": "has_phone",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "boolean"
                        },
                        "description": "Only return businesses with at least one phone number."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SearchResponse"
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/BadRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PaymentRequired"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/QuotaExceeded"
                    }
                }
            }
        },
        "/categories": {
            "get": {
                "tags": [
                    "Categories"
                ],
                "summary": "Two-level category taxonomy",
                "description": "Two-level directory of schema.org business types. The `code` of any entry (parent or child) can be passed to /search as the `category` filter. Auth required; calls NEVER count against the monthly quotas. Cached 24h server-side.",
                "parameters": [],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CategoriesResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PaymentRequired"
                    }
                }
            }
        },
        "/locations": {
            "get": {
                "tags": [
                    "Locations"
                ],
                "summary": "Country / region / city directory",
                "description": "Three-level directory used to build `region` codes for /search. Countries are labels only (not searchable). Regions and cities carry searchable `code` values. Auth required, but calls to this endpoint NEVER count against the monthly search or business quotas. Cached 24h server-side.",
                "parameters": [],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/LocationsResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PaymentRequired"
                    }
                }
            }
        },
        "/business/{id}": {
            "get": {
                "tags": [
                    "Business"
                ],
                "summary": "Retrieve a business profile",
                "description": "Full business profile (hours, phones, websites, photos, services, amenities, reviews). Counts against the monthly business quota.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "LocalityBiz business id."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BusinessResponse"
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/BadRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/PaymentRequired"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/QuotaExceeded"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "API Key"
            },
            "apiKeyQuery": {
                "type": "apiKey",
                "in": "query",
                "name": "api_key"
            }
        },
        "schemas": {
            "QuotaSnapshot": {
                "type": "object",
                "properties": {
                    "limit": {
                        "type": "integer"
                    },
                    "used": {
                        "type": "integer"
                    },
                    "remaining": {
                        "type": "integer"
                    },
                    "resets_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "type": {
                        "type": "string",
                        "enum": [
                            "search",
                            "business"
                        ]
                    }
                }
            },
            "ErrorEnvelope": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "enum": [
                            false
                        ]
                    },
                    "error": {
                        "type": "object",
                        "properties": {
                            "code": {
                                "type": "string"
                            },
                            "message": {
                                "type": "string"
                            },
                            "details": {
                                "type": "object"
                            }
                        }
                    },
                    "quota": {
                        "$ref": "#/components/schemas/QuotaSnapshot"
                    }
                }
            },
            "SearchResultItem": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "name": {
                        "type": "string"
                    },
                    "address": {
                        "type": "string"
                    },
                    "lat": {
                        "type": "number",
                        "nullable": true
                    },
                    "lng": {
                        "type": "number",
                        "nullable": true
                    },
                    "distance_km": {
                        "type": "number",
                        "nullable": true
                    },
                    "phone": {
                        "type": "string",
                        "nullable": true
                    },
                    "rating": {
                        "type": "number",
                        "nullable": true
                    },
                    "ratings_count": {
                        "type": "integer"
                    },
                    "main_category": {
                        "type": "string"
                    },
                    "details_url": {
                        "type": "string",
                        "format": "uri"
                    }
                }
            },
            "SearchResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "enum": [
                            true
                        ]
                    },
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/SearchResultItem"
                        }
                    },
                    "meta": {
                        "type": "object",
                        "properties": {
                            "page": {
                                "type": "integer"
                            },
                            "page_size": {
                                "type": "integer",
                                "description": "Number of items actually returned in `data` for this response. Equals the configured per-page cap on full pages, less on the last page, and 0 when nothing matched."
                            },
                            "query": {
                                "type": "object"
                            },
                            "next_page": {
                                "type": "string",
                                "format": "uri",
                                "description": "Present only when a next page exists."
                            }
                        }
                    },
                    "quota": {
                        "$ref": "#/components/schemas/QuotaSnapshot"
                    }
                }
            },
            "CategoriesResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "enum": [
                            true
                        ]
                    },
                    "data": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "code": {
                                    "type": "string",
                                    "description": "Parent category code. Pass to /search as `category` for a broad match."
                                },
                                "name": {
                                    "type": "string"
                                },
                                "children": {
                                    "type": "array",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "code": {
                                                "type": "string",
                                                "description": "Child category code. Pass to /search as `category` for a narrow match."
                                            },
                                            "name": {
                                                "type": "string"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "LocationsResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "enum": [
                            true
                        ]
                    },
                    "data": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "country": {
                                    "type": "string",
                                    "description": "Country name. NOT searchable on its own — pick a region instead."
                                },
                                "regions": {
                                    "type": "array",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "code": {
                                                "type": "string",
                                                "description": "Region code. Pass this verbatim as `region` in /search to scope to the whole region."
                                            },
                                            "name": {
                                                "type": "string"
                                            },
                                            "cities": {
                                                "type": "array",
                                                "items": {
                                                    "type": "object",
                                                    "properties": {
                                                        "code": {
                                                            "type": "string",
                                                            "description": "City code in the form \"<regionId>_<cityId>\". Pass as `region` in /search to scope to a single city."
                                                        },
                                                        "name": {
                                                            "type": "string"
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "BusinessResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "enum": [
                            true
                        ]
                    },
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "integer"
                            },
                            "name": {
                                "type": "string"
                            },
                            "rating": {
                                "type": "number",
                                "nullable": true
                            },
                            "ratings_count": {
                                "type": "integer"
                            },
                            "phones": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "websites": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "emails": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "address": {
                                "type": "object"
                            },
                            "coords": {
                                "type": "object"
                            },
                            "categories": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "description": {
                                "type": "string",
                                "nullable": true
                            },
                            "photos": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "hours": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "services": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "amenities": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "reviews": {
                                "type": "array",
                                "items": {
                                    "type": "object"
                                }
                            },
                            "urls": {
                                "type": "object"
                            }
                        }
                    },
                    "quota": {
                        "$ref": "#/components/schemas/QuotaSnapshot"
                    }
                }
            }
        },
        "responses": {
            "BadRequest": {
                "description": "Invalid parameters.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        }
                    }
                }
            },
            "Unauthorized": {
                "description": "Missing or invalid API key.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        }
                    }
                }
            },
            "Forbidden": {
                "description": "IP not whitelisted or scope denied.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        }
                    }
                }
            },
            "NotFound": {
                "description": "Resource not found.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        }
                    }
                }
            },
            "PaymentRequired": {
                "description": "Subscription is inactive.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        }
                    }
                }
            },
            "QuotaExceeded": {
                "description": "Monthly quota exceeded. Retry-After header points at the next billing period.",
                "headers": {
                    "Retry-After": {
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Seconds until quota resets."
                    }
                },
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        }
                    }
                }
            }
        }
    }
}