API для разработчиков

Публичный API мониторинга объектов недвижимости. Для доступа к данным необходим бесплатный API-токен — получите его ниже за несколько секунд.

Получить API-токен

Бесплатно, без регистрации. Обязателен он для SSE-стрима (/events/stream) и пакетного запроса (POST /objects/batch); остальные data-эндпоинты работают и без токена, но анонимные запросы ограничены общим лимитом на IP — по умолчанию 120 запросов в минуту с часовым баном при превышении, против 600 запросов в минуту на токен без бана. Оба значения — настраиваемые администратором дефолты (кэш 60 с), а не жёсткие константы, и могут измениться без предупреждения. Токенный лимит применяется при любом из двух способов передать токен — заголовком X-API-Key или query-параметром api_token (единственный способ передать его в EventSource для SSE). Токен при этом должен быть действующим: недействительный уходит под общий лимит на IP, наличия заголовка или параметра самого по себе недостаточно. Authorization: Bearer под токенный лимит не попадает — в этом заголовке приходит и сессионный токен личного кабинета.

Токен действителен до тех пор, пока активно используется. После 90 дней без обращений токен удаляется автоматически. При злоупотреблениях (нарушения rate limit) токен может быть отозван.

Базовая информация

Base URL: /api/v1

Swagger UI: /api/public/docs

OpenAPI spec: /api/public/openapi.json

Условия использования

  • Бесплатный API-токен — выдаётся мгновенно, регистрация не нужна
  • Коммерческое использование данных допустимо с соблюдением авторских прав первоисточников
  • Rate limit: по умолчанию не более 600 запросов в минуту на токен — заголовком X-API-Key или query-параметром ?api_token=, лимит один и тот же. Администратор может изменить его в любой момент
  • Размер тела запроса — не более 15 МБ; всё, что больше, отбивается кодом 413 ещё до разбора и обработки
  • SLA не гарантируется — API предоставляется «как есть»
  • Сервис не несёт ответственности за действия, совершённые на основе данных API

Sandbox — тестовая среда

Для разработки и отладки интеграции доступен sandbox — основные публичные эндпоинты с случайными моковыми данными. Sandbox не обращается к базе данных и не требует авторизации, но подчиняется тому же общему лимиту на IP, что и остальной API (по умолчанию 120 запросов в минуту). SSE-стрим непрерывно генерирует случайные события каждые 3–7 секунд.

Base URL: /api/v1/sandbox

SSE-стрим: /api/v1/sandbox/events/stream

Индекс эндпоинтов: /api/v1/sandbox

Пример — объекты

curl "/api/v1/sandbox/objects?page_size=3"

# Ответ — случайные объекты каждый раз
# { "total": 142, "page": 1, "items": [...] }

Пример — SSE-стрим

curl -N "/api/v1/sandbox/events/stream"

# event: REGISTRATION_OPENED
# id: sandbox-1
# data: {"event_id":"...","developer_id":"pionier",...}

# event: OBJECT_UPDATED
# id: sandbox-2
# data: {"event_id":"...","changed_fields":"[...]",...}

Доступные sandbox-эндпоинты повторяют структуру основного API: /developers, /developers/{id}/objects, /events, /events/stream, /registration-status, /health и другие. Полный список — в индексе sandbox.

Справочник эндпоинтов

GET/developers

Опубликованные застройщики, включая приостановленных (active: false). objects_count — объекты застройщика плюс те, где он указан соавтором или причастным; снятые с продажи и карточки-контейнеры (дома и ЖК) не в счёт; buildings_count и complexes_count — дома и жилые комплексы застройщика

Параметры:

  • в ответе contacts — контакты застройщика в том же формате, что у карточки услуги: phones, emails, websites, websites_display (домен без схемы, для показа) и social_links
  • в ответе юрблок: legal_form, legal_name, unp (в каталоге услуг то же поле зовётся tax_id), registered_year и legal_checked_at — дата сверки. legal_form приходит кодом из закрытого набора: ooo, odo, oao, zao, ip, self_employed, individual. Юрданные вводятся руками или берутся из открытого реестра, поэтому без даты сверки читать их как факт нельзя. Юрадреса здесь нет
  • в ответе is_claimed — карточку забрал представитель застройщика; is_verified — владелец подтвердил доступ к карточке. Ни то, ни другое не означает, что данные сверены нами с реестром
  • ответ кэшируется на 60 секунд

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/developers"

Ответ

[
  {
    "id": "ghb",
    "name": "ГХБ",
    "website": "https://ghb.by",
    "telegram_channel": "https://t.me/ghb_objects",
    "active": true,
    "registration_enabled": true,
    "objects_count": 245,
    "buildings_count": 12,
    "complexes_count": 3,
    "contacts": {
      "phones": ["+375291234567"],
      "emails": [],
      "websites": ["https://ghb.by"],
      "websites_display": ["ghb.by"],
      "social_links": []
    },
    "legal_form": "ooo",
    "legal_name": "ГХБ",
    "unp": "100000000",
    "registered_year": 2005,
    "legal_checked_at": "2026-08-20",
    "is_claimed": false,
    "is_verified": false,
    "last_check_at": "2026-03-29T10:15:00Z"
  }
]
GET/developers/{id}/destinations

Целевые направления застройщика: Telegram-каналы, Threads-аккаунты, Viber-каналы единым списком

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/developers/ghb/destinations"

Ответ

[
  {
    "platform": "telegram",
    "title": "ГХБ Новостройки",
    "username": "ghb_objects",
    "url": "https://t.me/ghb_objects",
    "visibility": "public",
    "member_count": 3200
  },
  {
    "platform": "viber",
    "title": "ГХБ Viber",
    "username": null,
    "url": null,
    "visibility": "private",
    "member_count": null
  }
]
GET/developers/{id}/channels

Только Telegram-каналы застройщика. Для всех платформ разом используйте /developers/{id}/destinations

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/developers/ghb/channels"

Ответ

[
  {
    "id": "...",
    "title": "ГХБ Новостройки",
    "type": "public_channel",
    "visibility": "public",
    "username": "ghb_objects",
    "url": "https://t.me/ghb_objects",
    "member_count": 3200
  }
]
GET/developers/{id}/objects

Объекты застройщика с фильтрацией и пагинацией

Параметры:

  • search — по названию, external_id или адресу
  • status, object_type, object_type_slug, registration_open, building, city
  • building_id — фильтр по дому как сущности (в отличие от building — корпуса из данных застройщика)
  • фильтра по жилому комплексу здесь нет: complex_id принимает только сквозной /objects — за объектами комплекса идите туда, /objects?developer_id=ghb&complex_id=UUID
  • min_price, max_price, min_area, max_area, rooms
  • sort (price_asc | price_desc | area_asc | area_desc | updated_at_desc)
  • group=building — свернуть квартиры одного дома в одну запись ленты; форма ответа та же, что у /objects?group=building (см. ниже). Дом сворачивается, только если под фильтр подходит 2+ его объекта
  • дома и жилые комплексы, отданные источником отдельной карточкой, из выдачи исключены всегда, в том числе в ленте group=building: у них нет ни комнат, ни площади, ни цены. По прямой ссылке /developers/{id}/objects/{ext_id} такая карточка по-прежнему открывается
  • attribution — own (по умолчанию) отдаёт объекты застройщика, participated — чужие объекты, где он указан соавтором или причастным
  • page, page_size (макс. 100)
  • в ответе currency — валюта price, estimated_price и price_per_sqm; заполнена всегда (умолчание застройщика либо переопределение объекта); сортировка price_asc/price_desc и фильтры min_price/max_price сравнивают не эту валюту, а BYN-эквивалент по последнему известному курсу НБРБ

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/developers/ghb/objects?registration_open=true&sort=price_asc"

Ответ

{
  "total": 3,
  "page": 1,
  "page_size": 50,
  "items": [{ "external_id": "OBJ-101", "title": "...", ... }]
}
GET/developers/{id}/objects/{ext_id}

Один объект по его external_id

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/developers/ghb/objects/OBJ-101"

Ответ

{ "external_id": "OBJ-101", "title": "...", "price": "85000.00", "currency": "BYN", ... }
GET/objects/{object_id}/history

Лента изменений объекта. object_id — внутренний UUID объекта (поле id в схеме объекта), а не пара developer_id/external_id

Параметры:

  • limit (1–200, по умолчанию 50)
  • before — вернуть события до этой даты (ISO datetime)
  • fields — ограничить историю списком полей через запятую

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/objects/3fa85f64-5717-4562-b3fc-2c963f66afa6/history"

Ответ

{
  "object_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "sources": [{ "developer_slug": "ghb", "external_id": "OBJ-101" }],
  "entries": [
    {
      "occurred_at": "...",
      "event_type": "OBJECT_UPDATED",
      "changes": [{ "field": "price", "old": "84000.00", "new": "85000.00" }]
    }
  ],
  "next_before": null
}
GET/objects/{object_id}/price-history

Собранная серия отображаемой цены объекта для графика (не сырые события, как /history). object_id — внутренний UUID объекта, а не пара developer_id/external_id

Параметры:

  • max_points (2–2000, по умолчанию 2000) — жёсткий потолок: серия прореживается до одной точки в день, при необходимости остаются последние max_points точек
  • currency в ответе — валюта объекта (переопределение объекта важнее валюты застройщика); вся серия приведена к ней пересчётом по курсам НБРБ
  • rate_adjustments — участки серии, пересчитанные в currency по курсу НБРБ: before — до какого момента действует пересчёт (застройщик вёл цену в другой валюте до этой даты), from_currency — из какой валюты, rate_date/rate — курс НБРБ, использованный для пересчёта (может быть раньше before, если на этот день курс не публиковался)

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/objects/3fa85f64-5717-4562-b3fc-2c963f66afa6/price-history"

Ответ

{
  "object_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "currency": "BYN",
  "first_observed_at": "2026-03-14T10:00:00Z",
  "points": [
    { "date": "2026-03-14T10:00:00Z", "price": "84000.00", "kind": "total" },
    { "date": "2026-05-02T09:00:00Z", "price": "86400.00", "kind": "estimated" }
  ],
  "downsampled": false,
  "rate_adjustments": [
    { "before": "2026-05-02T09:00:00Z", "from_currency": "USD", "rate_date": "2026-05-01", "rate": "3.2000" }
  ]
}
GET/objects/{object_id}/gallery

Картинки карточки объекта в порядке источника. object_id — внутренний UUID объекта, а не пара developer_id/external_id

Параметры:

  • ответ — массив { section, image_url, thumb_url }, та же форма, что у /buildings/{building_id}/gallery
  • строки наполняет джоба gallery_sync из extra источника; у pzs это рендер и планировка квартиры
  • обложка объекта (поле image_url карточки) входит в галерею первым кадром, дублировать её на клиенте не нужно
  • пока картинка не перекачана в наше хранилище, адреса ведут на сайт источника
  • скрытый или архивированный застройщик даёт 404, как и несуществующий объект

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/objects/3fa85f64-5717-4562-b3fc-2c963f66afa6/gallery"

Ответ

[
  { "section": "photos", "image_url": "https://...", "thumb_url": "https://..." },
  { "section": "photos", "image_url": "https://...", "thumb_url": "https://..." }
]
GET/object-types

Дерево типов объектов — источник допустимых значений object_type_slug для фильтров /objects и /developers/{id}/objects

Параметры:

  • lang — язык подписи (по умолчанию ru)

Запрос

curl "/api/v1/object-types"

Ответ

[
  {
    "slug": "apartment",
    "label": "Квартира",
    "parent_slug": null,
    "is_leaf": false,
    "children": [
      { "slug": "apartment.studio", "label": "Студия", "parent_slug": "apartment", "is_leaf": true, "children": [] }
    ]
  }
]
GET/objects

Каталог объектов сразу по всем застройщикам (архивные и тестовые исключены)

Параметры:

  • developer_id — ограничить одним застройщиком
  • search, status, object_type, object_type_slug, registration_open, building, city
  • building_id — фильтр по дому как сущности (в отличие от building — корпуса из данных застройщика)
  • complex_id — фильтр по жилому комплексу; независим от building_id, потому что complex_id заполнен и у объектов без дома
  • min_price, max_price, min_area, max_area, rooms
  • finishing — отделка: none (без отделки), pre_finish (предчистовая), finished (с отделкой); объекты, у которых источник отделку не сообщил, не попадают ни в один срез
  • completion — пресет по сроку сдачи: delivered (сдан), this_year (в этом году), next_year (в следующем), later (позже); объекты без срока сдачи не попадают ни в один пресет
  • sort (price_asc | price_desc | area_asc | area_desc | updated_at_desc)
  • дома и жилые комплексы, отданные источником отдельной карточкой, из плоского списка исключены: у них нет ни комнат, ни площади, ни цены. По прямой ссылке /developers/{id}/objects/{ext_id} такая карточка по-прежнему открывается. В ленте group=building исключение действует не всегда — см. карточку ленты ниже
  • group=building — свернуть квартиры одного дома в одну запись ленты (см. ниже)
  • page, page_size (макс. 100)
  • в ответе currency — валюта price, estimated_price и price_per_sqm; в общем каталоге она разная у разных застройщиков, но сортировка по цене (price_asc/price_desc) и фильтры min_price/max_price сравнивают не эту валюту, а BYN-эквивалент по последнему известному курсу НБРБ; для валюты без известного курса объект уходит в конец сортировки, а не сравнивается сырым числом
  • в ответе finishing и construction_stage — отделка и стадия строительства объекта; construction_stage — ось, независимая от status: сданный дом продолжает продаваться, а null означает, что источник о стройке молчит, а не что она не идёт

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/objects?city=Минск&rooms=2&sort=price_asc"

Ответ

{
  "total": 1842,
  "page": 1,
  "page_size": 50,
  "items": [{ "developer_id": "ghb", "external_id": "OBJ-101", ... }]
}
GET/objects/recent

Объекты, впервые обнаруженные за последние N часов

Параметры:

  • since_hours — окно в часах (1–168, по умолчанию 24)
  • те же фильтры, что у /objects, кроме object_type_slug и complex_id — этих двух здесь нет. Группировки group=building здесь тоже нет: ответ всегда плоский список
  • дома и комплексы, отданные источником отдельной карточкой, исключены всегда — оговорки про ленту, что у /objects, здесь не действуют
  • sort — те же варианты, что у /objects, плюс first_seen_at_desc (значение по умолчанию)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/objects/recent?since_hours=48"

Ответ

{ "total": 17, "page": 1, "page_size": 50, "items": [...] }
GET/objects/cities

Список городов, по которым есть объекты — для фильтров

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/objects/cities"

Ответ

["Барановичи", "Брест", "Витебск", "Минск"]
GET/objects?group=building

Смешанная лента: квартиры одного дома приходят одной записью, одиночные объекты — отдельными

Параметры:

  • те же фильтры, что у /objects
  • total считает записи ленты, а не квартиры
  • дом сворачивается, если под фильтр подходит 2+ его объекта — либо если у дома есть собственная карточка-контейнер: тогда он приходит домом даже в одиночку. Всё остальное приходит обычным объектом
  • в отличие от плоского списка, карточка дома, отданная источником целиком, в ленту попадает — но только если это дом (не жилой комплекс), он привязан к сущности дома и у него есть цена за м². Как только запрошен любой из rooms, min_price, max_price, min_area, max_area, такие карточки снова исключаются: комнатность, цена квартиры и площадь к ним неприменимы
  • registration_open=true — исключение из предыдущего правила: дом с открытой регистрацией приходит и в ленту, и в плоский список, причём без требований к цене и привязке. У застройщика, публикующего дом целиком, регистрация открывается именно на дом (#1957)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/objects?group=building&city=Минск"

Ответ

{
  "total": 42,
  "page": 1,
  "page_size": 50,
  "items": [
    {
      "kind": "building",
      "building": {
        "id": "...", "developer_id": "ghb", "name": "ул. Ленина, 5",
        "matched_count": 12, "total_count": 40,
        "min_price": "100000.00", "max_price": "250000.00",
        "rooms_available": [1, 2, 3], "type_counts": { "apartment": 10, "commercial": 2 },
        "preview": [...]
      },
      "object": null
    },
    { "kind": "object", "building": null, "object": { ... } }
  ]
}
GET/complexes

Жилые комплексы со сводкой: сколько домов и объектов в продаже, диапазоны цен и площадей, доступная комнатность

Параметры:

  • developer_id — слаг застройщика
  • city
  • page, page_size (макс. 100)
  • сортировка одна — по названию комплекса, параметра нет
  • в выдачу попадает только ЖК, у которого есть хотя бы одно: видимая единица продажи, видимый дом (см. /buildings) либо живая карточка-контейнер — своя или дома комплекса. Цены карточка не требует: застройщик, публикующий комплекс целиком, мог её не опубликовать вовсе. ЖК вовсе без содержимого из выдачи исключён; снятые с продажи и аренда не в счёт
  • в ответе buildings_count и objects_count считают только видимое: снятые с продажи и карточки-контейнеры не в счёт
  • в ответе currency — валюта min_price и max_price; комплекс принадлежит одному застройщику, берётся его валюта, переопределения отдельных объектов в агрегаты не попадают
  • в ответе container_prices — цена за м² с карточек-контейнеров: как самого комплекса, так и его домов. Список непустой только у комплекса, где нет ни одной видимой единицы продажи. Строки те же, что у /buildings: object_type, min_price_per_sqm, max_price_per_sqm, vat_included, currency, min_area, max_area
  • в ответе container_content — содержание карточки самого комплекса: description, site_url, flats_total, area_range, ceiling_height, flats_available, sold_out и construction_stage. В отличие от container_prices карточки домов сюда не сводятся — эти поля описывают предмет карточки, а не набор: описание одного дома не описывает комплекс, и распроданный дом не делает распроданным весь ЖК. Заполняется по тому же правилу, что и цена, — только комплексу без видимых единиц продажи. Набор полей у застройщиков разный, поэтому пустое поле — норма, а не пробел в данных; null у всего объекта означает, что своей карточки нет
  • в ответе container_object_id — uuid карточки-контейнера комплекса, на которую ссылается его лента изменений (GET /objects/{object_id}/history). Карточки его домов сюда не попадают — каждая описывает свой дом, а не комплекс целиком. В отличие от container_prices и container_content заполняется у любого комплекса с такой карточкой, а не только у комплекса без единиц продажи
  • в ответе container_object_url — страница той же карточки у застройщика. Это не container_content.site_url: тот приходит из таблицы источника и ведёт на отдельный сайт комплекса
  • в ответе container_external_id — external_id той же карточки; вместе со слагом застройщика это ключ, по которому комплекс, опубликованный целиком, кладут в «Мои объекты». Карточки домов сюда не попадают, и заполняется поле по правилу container_prices — только у комплекса без видимых единиц продажи

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/complexes?developer_id=ghb&city=Минск"

Ответ

{ "total": 7, "page": 1, "page_size": 20, "items": [{ "id": "...", "developer_id": "ghb", "name": "Минск Мир", "city": "Минск", "district": "Октябрьский", "buildings_count": 7, "objects_count": 214, "min_price": "95000.00", "max_price": "410000.00", "currency": "BYN", "min_area": "28.40", "max_area": "121.00", "rooms_available": [1, 2, 3, 4], "image_url": null, "thumb_url": null, "container_prices": [], "container_content": { ... }, "container_object_id": "...", "container_object_url": "https://developer.example/projects/grand", "container_external_id": "zhk-grand" }] }
GET/complexes/{complex_id}

Сводка одного комплекса вместе со списком его домов

Параметры:

  • complex_id — UUID комплекса; 404, если его нет или застройщик скрыт
  • в ответе buildings — те же поля, что у /buildings; пустой список законен: часть источников публикует комплекс целиком, без разбивки на дома
  • список buildings и счётчик buildings_count подчиняются тому же правилу видимости, что и /buildings: дом без единиц продажи и без цены за метр на своей карточке-контейнере в список домов ЖК не попадает
  • в ответе container_prices — как у /complexes: цена за м² с карточек-контейнеров, только когда видимых единиц продажи нет вовсе
  • в ответе container_object_id, container_object_url и container_external_id — то же, что у /complexes

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/complexes/UUID"

Ответ

{ "id": "...", "developer_id": "ghb", "name": "Минск Мир", "buildings_count": 7, "objects_count": 214, "rooms_available": [1, 2, 3], "container_object_id": "...", "container_object_url": "https://developer.example/projects/grand", "container_external_id": "zhk-grand", "buildings": [{ "id": "...", "name": "ул. Ленина, 5", "complex_id": "...", "complex_name": "Минск Мир", ... }] }
GET/buildings

Дома со сводкой: диапазоны цен и площадей, доступная комнатность, счётчики

Параметры:

  • developer_id — слаг застройщика
  • ids — список id домов через запятую, до 100 за запрос; пагинация при этом не применяется
  • city
  • kind — residential | parking | mixed. Без него паркинги в выдачу не попадают: выдача новостроек по умолчанию жилая. Спросите kind=parking явно — получите их. На выборку по ids это ограничение не распространяется: там возвращаются ровно запрошенные дома, паркинги в том числе
  • page, page_size (макс. 100)
  • в ответе kind — то же значение residential | parking | mixed; mixed у дома, где есть и квартиры, и паркинг
  • в ответе completion_date — срок сдачи дома (может быть null)
  • в ответе image_url — изображение дома, thumb_url — его уменьшенная копия для списков (может быть null)
  • в ответе currency — валюта min_price и max_price; берётся у застройщика дома, переопределения отдельных объектов в агрегаты не попадают
  • в ответе type_counts — состав дома под текущим фильтром: тип объекта → сколько его; счётчик в карточке дома подписывается по нему, а не словом «квартир»
  • в ответе street, house, quarter — компоненты адреса, разобранные парсером, рядом с полной строкой address. address приходит от источника как есть и может не нести номер дома — house заполняется отдельно, оба поля нужны, чтобы его показать
  • в ответе complex_id и complex_name — жилой комплекс дома (см. /complexes); имя стоит рядом с id намеренно, чтобы карточка дома ставила ссылку на комплекс без второго запроса. Оба поля null, если источник не разбивает застройку на комплексы
  • total_count и matched_count считают только то, что видит покупатель: снятые с продажи и карточки-контейнеры (сам дом, сам комплекс) в счёт не идут
  • в ответе container_prices — цена за м² с собственной карточки дома. Список непустой только у дома, где видимых единиц продажи нет вовсе: есть настоящие квартиры — цену берут по ним, и второе, расходящееся число рядом не показывается. Строка на каждую пару «тип объекта + база НДС»: object_type, min_price_per_sqm, max_price_per_sqm, vat_included (null — источник базу не указал), currency, min_area и max_area той же группы
  • в ответе container_external_id — external_id той же карточки-контейнера дома; вместе со слагом застройщика — ключ, по которому дом, опубликованный целиком, можно положить в «Мои объекты». Заполняется по тому же правилу, что и container_prices — только у дома без видимых единиц продажи
  • в ответе container_object_id — uuid карточки-контейнера дома, на которую ссылается его лента изменений (GET /objects/{object_id}/history). В отличие от container_external_id и container_prices заполняется у любого дома с такой карточкой, а не только у пустого: события карточки существуют независимо от того, видны ли под домом квартиры
  • в ответе container_object_url — страница той же карточки у застройщика: у дома, который публикуется целиком, карточки квартиры не существует, и другого выхода на источник у него нет. Заполняется по тому же правилу, что и container_object_id
  • в ответе registration_open, registration_url и registration_start_date — запись с карточки-контейнера дома. В отличие от container_prices считается у любого дома, а не только у пустого: у застройщика, публикующего дом целиком, запись открывается на дом, а не на квартиру (#1957). До открытия флаг ещё false и ссылки нет — остаётся только дата старта (#2001)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/buildings?developer_id=ghb"

Ответ

[{ "id": "...", "name": "ул. Ленина, 5", "kind": "residential", "completion_date": "2027-06-30", "address": "ул. Ленина, 5", "street": "Ленина", "house": "5", "total_count": 40, "min_price": "100000.00", "currency": "BYN", "type_counts": { "apartment": 38, "parking": 2 }, "container_prices": [], "container_external_id": null, "container_object_id": "...", "container_object_url": "https://developer.example/houses/18", ... }]
GET/buildings/{building_id}

Сводка одного дома

Параметры:

  • в ответе street, house, quarter — компоненты адреса из парсера рядом с полной строкой address (см. /buildings)
  • в ответе complex_id и complex_name — жилой комплекс дома, см. /buildings
  • в ответе kind, completion_date и container_prices — те же поля, что у /buildings
  • в ответе container_external_id, container_object_id и container_object_url — те же поля, что у /buildings
  • в ответе registration_open, registration_url и registration_start_date — те же поля, что у /buildings

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/buildings/UUID"

Ответ

{ "id": "...", "developer_id": "ghb", "name": "ул. Ленина, 5", "kind": "residential", "completion_date": "2027-06-30", "address": "ул. Ленина, 5", "street": "Ленина", "house": "5", "rooms_available": [1, 2], "type_counts": { "apartment": 12 }, "container_prices": [], "container_external_id": null, "container_object_id": "...", "container_object_url": "https://developer.example/houses/18", "preview": [...], ... }
GET/buildings/{building_id}/objects

Объекты дома — те же фильтры и сортировки, что у каталога

Параметры:

  • search, status, object_type, object_type_slug, registration_open, city
  • min_price, max_price, min_area, max_area, rooms
  • sort (по умолчанию price_asc)
  • page, page_size (макс. 100)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/buildings/UUID/objects?rooms=2"

Ответ

{ "total": 12, "page": 1, "page_size": 50, "items": [...] }
GET/buildings/{building_id}/price-table

Ценовые таблицы карточек-контейнеров дома — то, из чего сведены container_prices в сводке. Отдельной ручкой, а не полем сводки: у одного дома это до 89 строк, разбор идёт на лету при каждом запросе

Параметры:

  • ответ — массив таблиц; пустой список, если у дома нет карточек-контейнеров с ценовыми таблицами
  • object_type — тип объекта карточки-источника
  • kind — list (поимённый список квартир: номер, этаж, площадь, цена за метр) или matrix (цены по этажам и группам площадей; площади строки здесь нет — она спрятана в названии группы)
  • headers — заголовки колонок как у источника; notes — подписи под таблицей, которые колонками не являются (условия оплаты и подобное)
  • vat_included (null — источник базу не указал) и currency — общие для всей таблицы, currency берётся у застройщика дома. Показываются только рублёвые (BYN) таблицы источника: где те же квартиры продублированы в долларах, долларовая копия отбрасывается — иначе одна квартира считалась бы дважды
  • rows[].cells — ячейки строки как есть (форм таблиц у источника одиннадцать, сводить их к общему набору полей значило бы часть данных выбросить); rows[].area, price_per_sqm, total_price — разобранные из cells числа, могут быть null, если соответствующей колонки не было
  • rows[].total_is_estimate — true, если total_price не пришёл из своей колонки источника, а посчитан нами как area × price_per_sqm

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/buildings/UUID/price-table"

Ответ

[
  {
    "object_type": "apartment",
    "kind": "list",
    "headers": ["№ квартиры", "Этаж", "Площадь, м²", "Цена, бел.руб. с НДС"],
    "rows": [
      {
        "cells": ["12", "3", "54.20", "162 600"],
        "area": "54.20",
        "price_per_sqm": "3000.00",
        "total_price": "162600.00",
        "total_is_estimate": false
      }
    ],
    "notes": ["Цена указана с учётом рассрочки"],
    "vat_included": true,
    "currency": "BYN"
  }
]
GET/buildings/{building_id}/gallery

Планировки, схемы и фотографии из карточек-контейнеров дома, в порядке источника

Параметры:

  • ответ — массив { section, image_url, thumb_url }
  • section — slug раздела источника: layouts, site_plan, photos, construction_progress или other; подписи разделов остаются на стороне клиента
  • снятая с продажи карточка-контейнер своих картинок не отдаёт — тем же правилом, что и container_prices
  • пока картинка не перекачана в наше хранилище, image_url и thumb_url ведут на сайт источника; после перекачки — на наш бакет

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/buildings/UUID/gallery"

Ответ

[
  { "section": "layouts", "image_url": "https://...", "thumb_url": "https://..." },
  { "section": "site_plan", "image_url": "https://...", "thumb_url": null }
]
GET/buildings/{building_id}/history

Лента «что менялось» по дому: те же события, что у ленты объекта, но по всем объектам дома и свёрнутые по циклу прогона — одна строка вместо двенадцати одинаковых

Параметры:

  • limit (1–200, по умолчанию 50) — работает как «не меньше»: страница дотягивается до границы цикла прогона, поэтому строк может прийти больше
  • before — вернуть события до этой даты (ISO datetime); для следующей страницы брать next_before из ответа
  • kind строки — objects_added, objects_removed, registration_opened, registration_closed, price_up, price_down или field_changed; field заполнен только у field_changed и изменений цены
  • scope — building (карточка дома/ЖК, отданная источником отдельно) или units (обычные объекты продажи)
  • objects_total — сколько объектов в строке всего, objects — первые десять из них
  • values — заполнено только для полей из белого списка (completion_date, price, price_per_sqm), у остальных null и строка остаётся голым счётчиком. form — pair (одно old/new на всю группу) или range (группа разошлась — range_min/range_max, разброс сдвига, а не сырые значения); currency — только у price и price_per_sqm
  • truncated: true — выборка упёрлась в потолок в 5000 событий, лента считана по срезу

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/buildings/UUID/history?limit=50"

Ответ

{
  "building_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "entries": [
    {
      "occurred_at": "2026-05-02T09:00:00Z",
      "kind": "price_up",
      "field": "price",
      "scope": "units",
      "objects_total": 12,
      "objects": [{ "external_id": "OBJ-101", "title": "1-комн., 42.5 м²" }],
      "values": { "form": "range", "old": null, "new": null, "range_min": "500.00", "range_max": "1200.00", "currency": "BYN" }
    },
    {
      "occurred_at": "2026-04-18T07:30:00Z",
      "kind": "registration_opened",
      "field": null,
      "scope": "building",
      "objects_total": 3,
      "objects": [...],
      "values": null
    }
  ],
  "next_before": "2026-04-18T07:30:00Z",
  "truncated": false
}
POST/objects/{developer_id}/{external_id}/share-urls

Ссылки «поделиться» карточкой объекта — по одной на трекаемый редирект /r/{token} на каждый запрошенный канал

Параметры:

  • тело запроса — { channels } (необязательно): список из tg_miniapp, tg_link, tg_forward, wa, vk, ok, copy_link, copy_text, native, qr; неизвестное значение — 422. Без поля создаются ссылки на все каналы
  • ответ — объект { channel: url, ..., text }: ключ — имя канала, значение — https://stroi.homes/r/{token}. Канал пропадает из ответа только если создать редирект не вышло (сбой инфраструктуры), а не потому что его не запросили
  • text — готовая подпись: адрес, тип/комнатность/площадь, цена, застройщик и ссылка на основной канал (tg_miniapp, иначе copy_link, иначе первый доступный)
  • developer_id — слаг застройщика, external_id — идентификатор объекта у источника; 404, если застройщик скрыт или объект не найден
  • токена не требует

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"channels":["tg_miniapp","copy_link"]}' \
  "/api/v1/objects/ghb/OBJ-101/share-urls"

Ответ

{
  "tg_miniapp": "https://stroi.homes/r/AbC12XyZ",
  "copy_link": "https://stroi.homes/r/Qw9Zt5Lm",
  "text": "📍 ул. Ленина, 5\n🏠 Квартира · 2-комн. · 54.2 м²\n💰 85 000 BYN\n🏗 ГХБ\n\nhttps://stroi.homes/r/AbC12XyZ"
}
POST/buildings/{building_id}/share-urls

Ссылки «поделиться» домом — для застройщика, публикующего дом целиком, у которого нет отдельной карточки квартиры (#2157)

Параметры:

  • тело запроса — { channels }, тот же формат и та же валидация, что у /objects/{developer_id}/{external_id}/share-urls
  • building_id — UUID дома; мусор в пути и отсутствующий дом — 404
  • text — адрес дома (город, если адреса нет), год сдачи (если есть), застройщик и ссылка; строка про цену показывается только если у дома она вообще есть — «По запросу» вместо неё не подставляется
  • токена не требует

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"channels":["tg_miniapp","copy_link"]}' \
  "/api/v1/buildings/UUID/share-urls"

Ответ

{
  "tg_miniapp": "https://stroi.homes/r/Nb3F8kPq",
  "copy_link": "https://stroi.homes/r/Ht7Ls2Wc",
  "text": "🏠 ул. Ленина, 5\n📍 ул. Ленина, 5\n🗓 Сдача в 2027\n🏗 ГХБ\n\nhttps://stroi.homes/r/Nb3F8kPq"
}
POST/complexes/{complex_id}/share-urls

То же для жилого комплекса, который источник публикует целиком (#2157)

Параметры:

  • тело запроса — { channels }, тот же формат, что у /objects/{developer_id}/{external_id}/share-urls
  • complex_id — UUID комплекса; мусор в пути и отсутствующий комплекс — 404
  • text — то же, что у дома, но иконка 🏢 и адрес — город комплекса
  • токена не требует

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{}' \
  "/api/v1/complexes/UUID/share-urls"

Ответ

{
  "tg_miniapp": "https://stroi.homes/r/Zx4Rp9Nc",
  "tg_link": "https://stroi.homes/r/Yv2Km6Ts",
  "copy_link": "https://stroi.homes/r/Ju8Dw1Fa",
  "text": "🏢 Минск Мир\n📍 Минск\n🏗 ГХБ\n\nhttps://stroi.homes/r/Zx4Rp9Nc"
}
POST/services/{service_id}/share-urls

Ссылки «поделиться» карточкой услуги

Параметры:

  • тело запроса — { channels }, тот же формат, что у /objects/{developer_id}/{external_id}/share-urls
  • service_id — UUID услуги; 404 у снятой с публикации, объединённой с другой карточкой или несуществующей
  • text — имя, первая категория (как есть, без перевода в человекочитаемую подпись), рейтинг и число отзывов, город (если есть) и ссылка
  • токена не требует

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"channels":["tg_miniapp","wa"]}' \
  "/api/v1/services/abc123/share-urls"

Ответ

{
  "tg_miniapp": "https://stroi.homes/r/Pl5Bx3Ge",
  "wa": "https://stroi.homes/r/Sr7Cn0Qi",
  "text": "👤 Плиточник Иван\n🏷 remont-pod-klyuch\n⭐ 4.7  18 отзывов\n📍 Минск\n\nhttps://stroi.homes/r/Pl5Bx3Ge"
}
GET/objects/{developer_id}/{external_id}/share-stats

Шеринги и переходы по объекту за период, разрезом по каналам

Параметры:

  • period — период в днях, 7–365, по умолчанию 30
  • ответ — { total_shares, total_opens, by_channel: [{ channel, shares, opens }], period_days }
  • opens считается по клик-таблице с ретеншеном 7 дней: при period больше недели shares придут за весь запрошенный период, а opens — только за последние 7 дней
  • developer_id — слаг застройщика; 404, если он скрыт. Существование объекта с данным external_id не проверяется — для неизвестного external_id ответ будет нулевым, а не 404
  • токена не требует

Запрос

curl "/api/v1/objects/ghb/OBJ-101/share-stats?period=30"

Ответ

{
  "total_shares": 12,
  "total_opens": 34,
  "by_channel": [
    { "channel": "tg_miniapp", "shares": 8, "opens": 25 },
    { "channel": "copy_link", "shares": 4, "opens": 9 }
  ],
  "period_days": 30
}
GET/services/{service_id}/share-stats

Шеринги и переходы по услуге за период, разрезом по каналам

Параметры:

  • period — период в днях, 7–365, по умолчанию 30
  • ответ — та же форма, что у /objects/{developer_id}/{external_id}/share-stats, с той же семидневной оговоркой про opens
  • service_id — существование не проверяется: для мусора в пути и несуществующей услуги ответ тоже нулевой, а не 404
  • токена не требует

Запрос

curl "/api/v1/services/abc123/share-stats?period=30"

Ответ

{
  "total_shares": 5,
  "total_opens": 11,
  "by_channel": [
    { "channel": "wa", "shares": 3, "opens": 7 },
    { "channel": "tg_miniapp", "shares": 2, "opens": 4 }
  ],
  "period_days": 30
}
GET/objects/suggestions

Подсказки для поисковой строки: названия, адреса и корпуса. Есть нечёткий поиск и раскрытие сокращений (ул., кв.)

Параметры:

  • q — строка запроса (обязательна)
  • developer_id — ограничить одним застройщиком
  • limit (1–20, по умолчанию 8)
  • карточки домов и комплексов в подсказки не попадают — как и в каталог

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/objects/suggestions?q=маяк"

Ответ

{ "items": [{ "text": "Минск Мир, дом Маяк", "kind": "title" }] }
POST/objects/batch

Пакетный запрос объектов по парам developer_id + external_id (до 100 за раз). Токен обязателен

Параметры:

  • Тело: { "items": [{ "developer_id": "ghb", "external_id": "OBJ-101" }] }

Запрос

curl -X POST -H "X-API-Key: $API_TOKEN" -H "Content-Type: application/json" \
  -d '{"items":[{"developer_id":"ghb","external_id":"OBJ-101"}]}' \
  "/api/v1/objects/batch"

Ответ

{
  "items": [{ "developer_id": "ghb", "external_id": "OBJ-101", ... }],
  "missing": [{ "developer_id": "ghb", "external_id": "OBJ-999" }]
}
GET/developers/{id}/reviews

Отзывы о застройщике со средней оценкой. Запись через это API недоступна: отзыв оставляет только владелец объекта застройщика, из приложения stroi.homes, под своим аккаунтом — токена для этого недостаточно

Параметры:

  • page, page_size (макс. 100)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/developers/ghb/reviews"

Ответ

{
  "total": 24,
  "rating_avg": 4.2,
  "items": [{ "id": "...", "rating": 5, "text": "...", "created_at": "..." }]
}
GET/developers/{id}/stats

Публичная статистика интереса к застройщику за период. Значения ниже порога (10) не показываются — приходит null

Параметры:

  • period — период в днях (7–365, по умолчанию 30)

Запрос

curl "/api/v1/developers/ghb/stats?period=30"

Ответ

{
  "developer_slug": "ghb",
  "period_days": 30,
  "page_views": 1420,
  "object_views_top": [{ "object_external_id": "OBJ-101", "title": "...", "views": 340 }],
  "clicks_to_developer": 58,
  "events_count": 12,
  "subscribers_approx": 3200
}
GET/developers/{id}/reputation

Факты мониторинга по застройщику: сдвиги заявленных сроков сдачи, переоценки, дома с наступившим сроком сдачи и свежесть данных. Источник — собственная история событий, не отзывы; оценок здесь нет, только наблюдённые факты с датами, формулировки собирает клиент

Параметры:

  • has_data — false означает, что блок показывать не стоит: по застройщику ничего не наблюдалось
  • observed_since, last_change_at — момент первого и последнего наблюдённого изменения отслеживаемых полей (срок сдачи, цена)
  • change_batches_90d — сколько раз за 90 дней мониторинг фиксировал изменения; единица счёта — цикл прогона, а не событие: один прогон меняет сразу много объектов
  • completion_shifts_total и completion_shifts — сдвиги срока сдачи; в каждом occurred_at, old_date, new_date, shift_days (положительное — срок отодвинулся, отрицательное — приблизился), objects_total и objects (до 10 объектов группы: external_id, title)
  • price_increases_total, price_decreases_total и price_moves — переоценки; в каждом occurred_at, direction (up | down), objects_total и objects — та же форма, до 10 штук
  • completion_reached_total и completion_reached_buildings — дома, чей заявленный срок сдачи уже наступил (статуса «сдан» в данных нет, утверждается только наступление срока); элемент — { name, completion_date }
  • truncated — true, если выборка событий упёрлась в потолок (20 000) и счётчики посчитаны по срезу
  • поля completion_date и price, скрытые в /field-configs (public_value_visibility=hidden), из фактов исключаются; если скрыты оба — 404

Запрос

curl "/api/v1/developers/ghb/reputation"

Ответ

{
  "has_data": true,
  "observed_since": "2026-01-10T08:00:00Z",
  "last_change_at": "2026-08-30T09:00:00Z",
  "change_batches_90d": 14,
  "completion_shifts_total": 2,
  "completion_shifts": [
    {
      "occurred_at": "2026-07-01T09:00:00Z",
      "old_date": "2027-03-31",
      "new_date": "2027-06-30",
      "shift_days": 91,
      "objects_total": 40,
      "objects": [{ "external_id": "OBJ-101", "title": "..." }]
    }
  ],
  "price_increases_total": 6,
  "price_decreases_total": 1,
  "price_moves": [
    {
      "occurred_at": "2026-08-30T09:00:00Z",
      "direction": "up",
      "objects_total": 5,
      "objects": [{ "external_id": "OBJ-205", "title": "..." }]
    }
  ],
  "completion_reached_total": 1,
  "completion_reached_buildings": [{ "name": "ул. Ленина, 5", "completion_date": "2026-06-30" }],
  "truncated": false
}
GET/developers/{id}/objects/{external_id}/stats

Просмотры конкретного объекта за период. null, если просмотров меньше порога (10)

Параметры:

  • period — период в днях (7–365, по умолчанию 30)

Запрос

curl "/api/v1/developers/ghb/objects/OBJ-101/stats?period=30"

Ответ

{ "object_external_id": "OBJ-101", "period_days": 30, "views": 340 }
GET/reviews/{entity_type}/{entity_id}

Лента отзывов о материале, доме/объекте или застройщике вместе со средней оценкой; entity_type — material | object | developer, entity_id — внутренний UUID сущности, а у застройщика ещё и его слаг (наружу застройщик отдаётся только слагом). Это же верно для всех ручек ниже с entity_id в адресе. У объекта своих звёзд нет — оценка идёт по четырём аспектам (yard, noise, deadlines, finishing), и rating_avg тогда считается как средняя по средним аспектов, а не отдельная звёздная шкала. can_review / can_review_reason показывают, вправе ли текущий пользователь оставить отзыв и если нет — почему: anonymous (нет PWA Bearer), no_estimate, not_in_projects, too_recent, not_owner, unknown_entity, already_reviewed. Запись отзыва работает не как в остальном API: пишет только аккаунт приложения stroi.homes и лишь при выполненном предметном условии — см. ручки записи ниже, они, как и /developers/{id}/reviews выше, обычным API-токеном не открываются

Параметры:

  • page (по умолчанию 1), page_size (1–100, по умолчанию 20)
  • sort — date | helpful; без параметра выбирается автоматически по числу отзывов сущности: helpful при более чем 10, иначе date — порог не зависит от текущих фильтров
  • with_photos — только отзывы с показанным (прошедшим модерацию) фото, по умолчанию false
  • critical — только с оценкой ниже 4 (у объекта — по средней аспектов отзыва), по умолчанию false

Запрос

curl "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6?sort=helpful"

Ответ

{
  "total": 32,
  "filtered_total": 32,
  "rating_avg": 4.25,
  "aspects": { "yard": 4.5, "noise": 3.5, "deadlines": 4.5, "finishing": 4.5 },
  "can_review": false,
  "can_review_reason": "already_reviewed",
  "sort": "helpful",
  "items": [
    {
      "id": "5c2e2b0a-...",
      "rating": null,
      "text": "Двор ухоженный, но слышно дорогу",
      "aspects": { "yard": 5, "noise": 2, "deadlines": 5, "finishing": 4 },
      "created_at": "2026-08-01T10:00:00Z",
      "can_delete": false,
      "reply_text": null,
      "reply_at": null,
      "reply_role": null,
      "photos": [{ "id": "a1b2c3d4-...", "url": "https://...", "thumb_url": "https://...", "status": "approved" }],
      "helpful_count": 6,
      "not_helpful_count": 1,
      "my_vote": true
    }
  ]
}
POST/reviews/{entity_type}/{entity_id}

Оставить отзыв. Запись через это API недоступна для сторонних интеграций: нужен аккаунт приложения (PWA Bearer, не X-API-Key из /developer-tokens) и выполненное предметное условие — материал был посчитан в смете пользователя, дом дольше месяца в «Моих объектах», у застройщика есть завершённая сделка (interest_status на пост-покупательной стадии). Одна сущность — один живой отзыв от аккаунта, отдельной квоты в минуту нет. Тело — либо rating (1–5) у материала и застройщика, либо aspects у объекта (обязательны все четыре: yard, noise, deadlines, finishing, каждое 1–5); смешать нельзя — 422. text необязателен, проходит автомодерацию

Параметры:

  • 422 "Entity is rated by aspects, not by stars" — прислан rating у объекта; 422 "Aspects required: …" — не хватает аспекта; 422 "Entity has no aspect ratings" / "Rating is required" — у материала и застройщика
  • 403 с причиной из can_review_reason выше, если право не выполнено; 409 already_reviewed, если отзыв уже есть; 404 — сущности нет

Запрос

curl -X POST -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PWA_TOKEN" \
  -d '{"aspects":{"yard":5,"noise":4,"deadlines":5,"finishing":4},"text":"Двор ухоженный"}' \
  "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6"

Ответ

{
  "id": "5c2e2b0a-...",
  "rating": null,
  "text": "Двор ухоженный",
  "aspects": { "yard": 5, "noise": 4, "deadlines": 5, "finishing": 4 },
  "created_at": "2026-09-05T10:00:00Z",
  "can_delete": true,
  "reply_text": null,
  "reply_at": null,
  "reply_role": null,
  "photos": [],
  "helpful_count": 0,
  "not_helpful_count": 0,
  "my_vote": null
}
DELETE/reviews/{entity_type}/{entity_id}/{review_id}

Удалить свой отзыв (мягко). Только автор — 403 «Not the author» иначе. Требует тот же PWA Bearer, что и при создании; API-токен не подходит. 204 без тела, 404 — отзыв не найден или относится к другой сущности

Запрос

curl -X DELETE -H "Authorization: Bearer $PWA_TOKEN" \
  "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/5c2e2b0a-..."

Ответ

204 No Content
POST/reviews/{entity_type}/{entity_id}/{review_id}/reply

Ответ представителя застройщика на отзыв о доме/объекте или о самом застройщике. У отзыва о материале второй стороны нет — ответить нельзя ни при каких правах. Отвечать может только участник с активным членством (DeveloperMembership) у соответствующего застройщика — 403 «Not a representative» без него. Пустой text снимает ответ. Требует PWA Bearer представителя, недоступно по API-токену

Параметры:

  • entity_type — object | developer (у material ответа не бывает)
  • Тело: { "text": "…" | null }

Запрос

curl -X POST -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PWA_TOKEN" \
  -d '{"text":"Спасибо за отзыв, передадим в УК"}' \
  "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/5c2e2b0a-.../reply"

Ответ

{
  "id": "5c2e2b0a-...",
  "rating": null,
  "text": "Двор ухоженный",
  "aspects": { "yard": 5, "noise": 4, "deadlines": 5, "finishing": 4 },
  "created_at": "2026-09-01T10:00:00Z",
  "can_delete": false,
  "reply_text": "Спасибо за отзыв, передадим в УК",
  "reply_at": "2026-09-05T12:00:00Z",
  "reply_role": "developer",
  "photos": [],
  "helpful_count": 6,
  "not_helpful_count": 1,
  "my_vote": null
}
POST/reviews/{entity_type}/{entity_id}/{review_id}/photos

Приложить фото к своему отзыву — до 5 неотклонённых на отзыв, сверх лимита 422 «Больше 5 фотографий к отзыву не приложить». Показано будет только после модерации (status: pending → approved/rejected); до решения фото видно лишь автору. Заливка идёт тем же конвейером, что у карточки услуги — файл жмётся в WebP. Доступно только автору отзыва (403 «Not the author»), только с PWA Bearer

Параметры:

  • multipart/form-data, поле file — изображение

Запрос

curl -X POST -H "Authorization: Bearer $PWA_TOKEN" \
  -F "file=@yard.jpg" \
  "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/5c2e2b0a-.../photos"

Ответ

{
  "id": "a1b2c3d4-...",
  "url": "https://...",
  "thumb_url": "https://...",
  "status": "pending"
}
DELETE/reviews/{entity_type}/{entity_id}/{review_id}/photos/{photo_id}

Снять своё фото у отзыва. Только автор отзыва, 404 — фото не найдено или относится к другому отзыву. PWA Bearer обязателен

Запрос

curl -X DELETE -H "Authorization: Bearer $PWA_TOKEN" \
  "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/5c2e2b0a-.../photos/a1b2c3d4-..."

Ответ

204 No Content
PUT/reviews/{entity_type}/{entity_id}/{review_id}/vote

«Помогло» / «не помогло» — голос смотрящего, не автора: 403 «Cannot vote for own review» на свой же отзыв. helpful: true | false | null, null снимает голос. PUT, а не POST/PATCH: состояние задаётся целиком, повторная отправка того же тела после обрыва сети ничего не ломает. PWA Bearer обязателен

Параметры:

  • Тело: { "helpful": true | false | null }

Запрос

curl -X PUT -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PWA_TOKEN" \
  -d '{"helpful":true}' \
  "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/5c2e2b0a-.../vote"

Ответ

{ "helpful_count": 7, "not_helpful_count": 1, "my_vote": true }
POST/reviews/{entity_type}/{entity_id}/{review_id}/report

Пожаловаться на отзыв — вход в очередь модератора, сам отзыв не меняется. Причины: spam, offensive, fake, personal_data, other. Отдельной квоты нет: повторная жалоба того же аккаунта на тот же отзыв не плодит строк, а аккаунт здесь обязателен — PWA Bearer, не API-токен

Параметры:

  • Тело: { "reason": "spam" | "offensive" | "fake" | "personal_data" | "other", "comment"?: "…" }

Запрос

curl -X POST -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PWA_TOKEN" \
  -d '{"reason":"fake"}' \
  "/api/v1/reviews/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/5c2e2b0a-.../report"

Ответ

{ "status": "pending" }
GET/services

Каталог специалистов и подрядчиков. Сортировка по умолчанию — relevance

Параметры:

  • q — поиск по названию, описанию и тегам
  • category, tag, country, city — фильтры справочников; карточки «по всей стране» попадают в выдачу любого города
  • district — район исполнителя (до 100 символов)
  • business — слаг бизнеса, витрина одной компании
  • verified, has_photo, has_reviews — булевы флаги, по умолчанию false
  • price_from, price_to (≥ 0), rating_min (1–5)
  • object_type, provider_type, feature — коды из /services/facets
  • sort (relevance | rating | reviews | price_asc | price_desc | new)
  • page, page_size (1–100, по умолчанию 20)

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/services?category=remont-pod-klyuch&city=Минск&sort=rating"

Ответ

{
  "total": 214,
  "page": 1,
  "page_size": 20,
  "items": [{ "id": "abc123", "name": "…", "categories": ["…"], "rating_avg": 4.7, ... }]
}
POST/services

Создать карточку специалиста или подрядчика — публично, токен не обязателен. name проходит автомодерацию (как и description, price_notes, tags). Владельцем карточки становится тот, кто её создал: если запрос несёт X-TG-Init-Data или PWA Bearer, дальше только с той же идентичностью можно будет отредактировать или удалить карточку через PATCH/DELETE ниже. Без них у карточки не будет публичного владельца вообще — поправить её потом сможет только админ или кто угодно через /services/{service_id}/suggestions. В ответе can_edit всегда false, даже для создателя — его считает только следующий запрос с той же идентичностью. Суточная квота на запись без токена — как у остальных публичных форм: у анонимов минимальная, у авторизованных выше, при превышении 429 и мини-игра-проверка; с API-токеном вместо неё действует общий per-token rate limit

Параметры:

  • Тело: name (обязательно); также, как у карточки: description, categories, custom_categories, tags, features, object_types, provider_type, attributes, locations, price_min, price_max, price_currency, price_unit, price_notes, contacts — все необязательны
  • Юрблок в теле: legal_form (код: ooo | odo | oao | zao | ip | self_employed | individual), legal_name, tax_id (УНП), registered_year (1900 — текущий год) и legal_checked_at. Прислать любое из первых четырёх без legal_checked_at нельзя — 422 «Юрданные принимаются только с датой сверки (legal_checked_at)»

Запрос

curl -X POST -H "Content-Type: application/json" \
  -H "X-TG-Init-Data: $INIT_DATA" \
  -d '{"name":"Плиточник Иван","categories":["remont-pod-klyuch"]}' \
  "/api/v1/services"

Ответ

{
  "id": "abc123",
  "name": "Плиточник Иван",
  "categories": ["remont-pod-klyuch"],
  "can_edit": false,
  "can_suggest": true,
  "status": "draft",
  ...
}
GET/services/{service_id}

Карточка услуги целиком: категории, теги, характеристики, локации, цены, контакты, изображения, услуги исполнителя и агрегаты отзывов. can_suggest — можно ли предложить правку карточки, пока её не забрал бизнес (см. /services/{service_id}/suggestions); custom_categories — категории карточки вне публичной таксономии, ожидающие модерации или отклонённые (утверждённые отдаёт /services/categories)

Параметры:

  • в ответе юрблок: legal_form, legal_name, tax_id (УНП; у застройщика то же поле зовётся unp), registered_year и legal_checked_at — дата сверки. legal_form приходит кодом из того же набора, что у застройщика: ooo, odo, oao, zao, ip, self_employed, individual. Записать юрданные без даты сверки нельзя (см. POST и PATCH ниже), на сайте блок без неё не показывается
  • offerings — услуги исполнителя, каждая со своей ценой, категорией и адресом: id, title, slug (считается из title при создании и дальше не меняется), category_slug, description, price_min, price_max, price_currency, price_unit, price_notes, sort_order, status. В публичном ответе только опубликованные (status: published) — черновики сюда не попадают

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/abc123"

Ответ

{
  "id": "abc123",
  "name": "Ремонт под ключ",
  "description": "…",
  "categories": ["remont-pod-klyuch", "warm-floor-install"],
  "tags": ["ремонт", "отделка"],
  "features": ["smeta"],
  "object_types": ["flat"],
  "provider_type": "brigada",
  "attributes": { "material": ["gipsokarton"] },
  "locations": [{ "country": "BY", "city": "Минск" }],
  "price_min": "1200.00",
  "price_max": "2500.00",
  "price_currency": "BYN",
  "price_unit": "sqm",
  "price_notes": null,
  "legal_form": "ip",
  "legal_name": "Иванов И. И.",
  "tax_id": "100000000",
  "registered_year": 2019,
  "legal_checked_at": "2026-08-20",
  "contacts": {
    "phones": ["+375291234567"],
    "emails": [],
    "websites": ["https://example.by"],
    "websites_display": ["example.by"],
    "social_links": []
  },
  "logo_url": null,
  "images": [],
  "offerings": [
    {
      "id": "…",
      "title": "Укладка плитки",
      "slug": "ukladka-plitki",
      "category_slug": "remont-pod-klyuch",
      "description": null,
      "price_min": "25.00",
      "price_max": "40.00",
      "price_currency": "BYN",
      "price_unit": "sqm",
      "price_notes": null,
      "sort_order": 0,
      "status": "published"
    }
  ],
  "social_stats": [],
  "rating_avg": 4.7,
  "review_count": 18,
  "positive_count": 16,
  "can_suggest": true,
  "custom_categories": [
    { "slug": "warm-floor-install", "label": "Тёплый пол", "parent_slug": null, "status": "pending" }
  ]
}
PATCH/services/{service_id}

Отредактировать карточку напрямую. Доступно только автору — тот же X-TG-Init-Data (Telegram) или тот же PWA Bearer, что были при создании, — либо сервисному API-токену админа (kind="service"); обычный публичный токен из POST /developer-tokens прав не даёт (403 «Only the author can modify this record»). 403 «Service is claimed by a business», если карточку уже забрал бизнес — правка тогда только в бизнес-портале. На практике доступно не всем: у карточки без зафиксированного при создании владельца (без X-TG-Init-Data/Bearer на POST /services) отредактировать её напрямую не может никто, кроме админа, — единственный публичный путь для таких карточек — POST /services/{service_id}/suggestions ниже. Сайт stroi.homes не отправляет ни X-TG-Init-Data, ни PWA Bearer, поэтому кнопки прямого редактирования на сайте нет — ручка для внешних интеграций и Mini App. Заполняются только присланные поля (частичное обновление), текст проходит автомодерацию, суточная квота на запись — как у POST /services выше

Параметры:

  • Тело: те же поля, что у создания (см. POST /services выше) — все необязательны, меняются только присланные
  • Юрблок правится теми же полями и с тем же условием: legal_form, legal_name, tax_id или registered_year в патче требуют legal_checked_at, иначе 422

Запрос

curl -X PATCH -H "Content-Type: application/json" \
  -H "X-TG-Init-Data: $INIT_DATA" \
  -d '{"price_min":1300}' \
  "/api/v1/services/abc123"

Ответ

{
  "id": "abc123",
  "price_min": "1300.00",
  "can_edit": true,
  ...
}
POST/services/{service_id}/suggestions

Предложить правку карточки — доступно кому угодно, авторизация не нужна, пока карточку не забрал бизнес (403, если забрал). Безопасная часть патча (справочники, известные категории/теги/города, цена в разумных пределах) применяется сразу; остальное уходит в очередь модерации. Суточная квота на запись — как у остальных публичных форм: у анонимов минимальная, у авторизованных выше, при превышении 429 и мини-игра-проверка. Отдельная суточная квота на автоприменения при исчерпании не отклоняет правку, а целиком переводит её в очередь

Параметры:

  • Тело: те же поля, что у карточки — name, description, categories, custom_categories, tags, features, object_types, provider_type, attributes, locations, price_min, price_max, price_currency, price_unit, price_notes, contacts и юрблок (legal_form, legal_name, tax_id, registered_year, legal_checked_at) — все необязательны, плюс comment (записка модератору). Условие даты сверки то же, что у PATCH

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"price_min":1300,"comment":"Подняли цену с сезона"}' \
  "/api/v1/services/abc123/suggestions"

Ответ

{
  "applied_fields": ["price_min"],
  "pending_fields": [],
  "suggestion_ids": ["b1f6c2b0-..."]
}
POST/services/{service_id}/reports

Пожаловаться на карточку: закрылись, дубль, спам. Замена публичного удаления — сама жалоба карточку не меняет, её разбирает админ. Действует та же суточная квота на автора, что и у других публичных форм. Отвечает 204 без тела

Параметры:

  • Тело: { "reason": "closed" | "duplicate" | "spam" | "other", "comment"?: "…" }

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"reason":"closed"}' \
  "/api/v1/services/abc123/reports"

Ответ

204 No Content
GET/catalog/{entity_type}/{entity_id}/suggestions

Какие поля объекта, дома или застройщика кто-то уже оспорил. Токен не нужен. В ответе нет ни предложенного текста, ни того, что стоит сейчас: «как правильно» пишет кто угодно, а показ непроверенного текста на карточке — готовое место под рекламу. Клиенту хватает знать, что поле оспорено

Параметры:

  • entity_type — object | building | developer
  • entity_id — внутренний UUID сущности (поле id в её схеме), а не external_id объекта. Невалидный UUID — 404; по хорошо сформированному UUID, которому ничего не соответствует, приходит 200 с пустым items — существование сущности эта ручка не проверяет
  • отдаются только правки, ожидающие модератора: до 50 штук, новые сверху

Запрос

curl "/api/v1/catalog/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/suggestions"

Ответ

{
  "items": [
    {
      "id": "b1f6c2b0-...",
      "field_name": "title",
      "status": "pending",
      "created_at": "2026-09-01T10:15:00Z"
    }
  ]
}
POST/catalog/{entity_type}/{entity_id}/suggestions

Предложить правку данных объекта, дома или застройщика — доступно кому угодно, авторизация не нужна. Автоприменения здесь нет вовсе, в отличие от карточек услуг: все правимые поля — свободный текст, то есть место под рекламу, поэтому каждая правка ждёт модератора и до его решения ничего в каталоге не меняет. Одна правка — одно поле: форма спрашивает «что не так → что сейчас → как правильно → откуда знаете». Суточная квота на запись общая с каталогом услуг и действует только без API-токена; с токеном вместо неё работает общий per-token rate limit

Параметры:

  • entity_type — object | building | developer; entity_id — внутренний UUID сущности
  • Тело: { "field_name": "…", "suggested_value": "…", "current_value"?: "…", "source_note"?: "…" }. suggested_value обязателен и не может быть пустым, current_value — до 512 символов
  • Правимые поля объекта: title, address, street, house, quarter, city, district
  • Правимые поля дома: name, city, address, street, house, quarter
  • Правимые поля застройщика: name, website, description
  • Список именно белый: числа, даты и перечисления не правятся (их пришлось бы приводить к типу при применении), контакты застройщика — тоже, потому что подмена телефона это прямой увод лидов. Поле вне списка — 422 «Field is not editable: <имя>»
  • Длина suggested_value ограничена самой колонкой; там, где у колонки нет своей длины, потолок 2000 символов. Превышение — 422. Несуществующая сущность — 404
  • 201 с телом правки: id, field_name, status (всегда pending), created_at

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"field_name":"title","current_value":"Нововиленская ул. д. 12.2 по ГП","suggested_value":"ул. Нововиленская, 12","source_note":"Табличка на доме"}' \
  "/api/v1/catalog/object/3fa85f64-5717-4562-b3fc-2c963f66afa6/suggestions"

Ответ

{
  "id": "b1f6c2b0-...",
  "field_name": "title",
  "status": "pending",
  "created_at": "2026-09-01T10:15:00Z"
}
POST/catalog/{entity_type}/{entity_id}/reports

Сообщить о проблеме с объектом, домом или застройщиком: дубль, снятая позиция, чужие фото. Жалоба сама ничего не меняет — её разбирает админ. Авторизация не нужна, та же суточная квота на запись, что у правки выше. Отвечает 204 без тела

Параметры:

  • entity_type — object | building | developer; entity_id — внутренний UUID сущности
  • Тело: { "reason": "duplicate" | "removed" | "wrong_photos" | "other", "comment"?: "…" } — набор причин свой, он не совпадает с набором у /services/{service_id}/reports
  • Несуществующая сущность — 404

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"reason":"duplicate","comment":"Тот же объект уже есть"}' \
  "/api/v1/catalog/building/3fa85f64-5717-4562-b3fc-2c963f66afa6/reports"

Ответ

204 No Content
DELETE/services/{service_id}

Удалить карточку (soft-delete) — то же право доступа, что у PATCH выше: только автор той же идентичности или сервисный токен админа, иначе 403 «Only the author can modify this record»; 403 «Service is claimed by a business», если её забрал бизнес. Доступно на практике только для карточек с зафиксированным при создании владельцем — как и у PATCH, у карточек без него удалить может лишь админ. Суточная квота на запись — как у POST /services выше. Тело запроса необязательное: старый клиент, который его не шлёт, удаляет карточку как раньше, без причины (#1932). 204 без тела, 404 — если карточки уже нет

Параметры:

  • Тело (необязательно): { "reasons"?: ["reasonStale"|"reasonDuplicate"|"reasonIncorrect"|"reasonStopped"|"reasonRules", ...], "comment"?: "…" }
  • reasons — неизвестные ключи отбрасываются, а не отклоняют запрос; comment обрезается до 500 символов. Оба поля служебные: их видит только админ, наружу карточка их не отдаёт

Запрос

curl -X DELETE -H "X-TG-Init-Data: $INIT_DATA" -H "Content-Type: application/json" \
  -d '{"reasons":["reasonStopped"],"comment":"Стройка заморожена"}' \
  "/api/v1/services/abc123"

Ответ

204 No Content
GET/services/{service_id}/reviews

Отзывы о карточке услуги вместе со средней оценкой. Скрытые модерацией отзывы не отдаются

Параметры:

  • page, page_size (1–100, по умолчанию 20)
  • is_verified в элементах — отзыв связан с закрытой заявкой к этой же карточке от того же PWA-аккаунта; сам lead_id наружу не отдаётся

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/abc123/reviews"

Ответ

{
  "total": 18,
  "rating_avg": 4.7,
  "items": [
    {
      "id": "…",
      "rating": 5,
      "text": "…",
      "created_at": "2026-05-02T09:00:00Z",
      "can_delete": false,
      "business_reply": "Спасибо!",
      "business_reply_at": "2026-05-03T10:00:00Z",
      "is_verified": true
    }
  ]
}
POST/services/{service_id}/reviews

Оставить отзыв о карточке услуги — публично, токен не обязателен. rating (1–5) обязателен, text — свободный, проходит автомодерацию. Тело может нести lead_id — заявку, из закрытия которой отправлен отзыв: связь ставится только при PWA Bearer у автора отзыва и только если заявка закрыта, оформлена к этой же карточке услуги и её автор — тот же PWA-аккаунт; без PWA Bearer, с чужой заявкой или заявкой к другой карточке связь не устанавливается, а отзыв уходит обычным анонимным — невалидный или чужой lead_id отзыв не блокирует. Повторные отзывы одного автора не ограничены — здесь нет правила «один отзыв на сущность», которое действует в общем тракте отзывов (/reviews/{entity_type}/{entity_id}) выше. can_delete в ответе не декоративен: у чисто анонимного автора (без X-TG-Init-Data и PWA Bearer) он всегда false, потому что авторство никогда не определяется по IP — такой отзыв автор не сможет удалить сам, только админ через сервисный токен. Суточная квота на запись без токена — как у остальных публичных форм каталога, при превышении 429 и мини-игра-проверка. 404, если карточки нет

Параметры:

  • Тело: { "rating": 1..5, "text"?: "…", "lead_id"?: "…" }
  • Ответ (201) — та же форма, что у элементов items в GET .../reviews выше, включая is_verified

Запрос

curl -X POST -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PWA_TOKEN" \
  -d '{"rating":5,"text":"Отличная работа","lead_id":"5c2e2b0a-..."}' \
  "/api/v1/services/abc123/reviews"

Ответ

{
  "id": "5c2e2b0a-...",
  "rating": 5,
  "text": "Отличная работа",
  "created_at": "2026-09-01T10:15:00Z",
  "can_delete": false,
  "business_reply": null,
  "business_reply_at": null,
  "is_verified": true
}
DELETE/services/{service_id}/reviews/{review_id}

Удалить свой отзыв (мягко). Право — как у PATCH/DELETE карточки услуги выше: совпадение Telegram- или PWA-идентичности с автором отзыва, либо сервисный токен админа, иначе 403 «Only the author can modify this record». Анонимный отзыв (без X-TG-Init-Data/PWA Bearer при создании) под это правило не подпадает никогда — авторство по IP не считается, снять такой отзыв может только админ. review_id обязан принадлежать сервису из пути, иначе 404 (#2404). Суточная квота на запись — как у остальных публичных форм. 204 без тела, 404 — если отзыва уже нет

Запрос

curl -X DELETE -H "X-TG-Init-Data: $INIT_DATA" \
  "/api/v1/services/abc123/reviews/5c2e2b0a-..."

Ответ

204 No Content
POST/services/{service_id}/reviews/{review_id}/report

Пожаловаться на отзыв — вход в очередь модератора, сам отзыв не меняется. Причины — те же пять, что у общего тракта отзывов: spam, offensive, fake, personal_data, other; reason принимается как произвольная строка, а не enum, поэтому неизвестное значение отклоняет не стандартный pydantic-конверт, а 422 от обработчика каталожных правил. Жалоба Telegram- или PWA-идентичности дедуплицируется — повторная жалоба того же аккаунта на тот же отзыв не плодит строк и возвращает уже существующую запись; анонимная (по IP) не дедуплицируется — от накрутки её защищает только суточная квота на запись, как у остальных публичных форм. Ответ (201) — { "status": "pending" }. 404, если отзыва нет или он принадлежит другому сервису (#2404)

Параметры:

  • Тело: { "reason": "spam" | "offensive" | "fake" | "personal_data" | "other", "comment"?: "…" }

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"reason":"spam","comment":"Похоже на накрутку"}' \
  "/api/v1/services/abc123/reviews/5c2e2b0a-.../report"

Ответ

{ "status": "pending" }
POST/services/{service_id}/reviews/{review_id}/reply

Публичный ответ владельца карточки на отзыв. Право — совпадение Telegram- или PWA-идентичности с автором карточки (не отзыва), либо сервисный токен админа, иначе 403 «Only the author can modify this record»; в отличие от PATCH/DELETE карточки выше, claim бизнесом (business_id) здесь не проверяется — решает только совпадение автора карточки. review_id должен принадлежать указанному service_id, иначе 404 «Review not found» — ту же сверку с #2404 делают DELETE и жалоба. Пустой или отсутствующий text снимает ответ. Суточная квота на запись — как у остальных публичных форм, действует и для владельца. Ответ — полная форма отзыва, как у items в GET .../reviews, с обновлёнными business_reply и business_reply_at

Параметры:

  • Тело: { "text": "…" | null }

Запрос

curl -X POST -H "Content-Type: application/json" \
  -H "X-TG-Init-Data: $INIT_DATA" \
  -d '{"text":"Спасибо за отзыв!"}' \
  "/api/v1/services/abc123/reviews/5c2e2b0a-.../reply"

Ответ

{
  "id": "5c2e2b0a-...",
  "rating": 5,
  "text": "Отличная работа",
  "created_at": "2026-09-01T10:15:00Z",
  "can_delete": false,
  "business_reply": "Спасибо за отзыв!",
  "business_reply_at": "2026-09-01T11:00:00Z"
}
POST/challenge

Выдать одно задание мини-игры-проверки — анаграмму или квиз по строительной терминологии, наугад из типов, включённых в админке (chal:games_enabled). Токен и авторизация не нужны; ожидаемый ответ хранится только в Redis и в ответе не отдаётся, так что сам запрос его не подсказывает. Задание живёт 300 секунд и допускает 3 неверных попытки — по истечении срока или после третьей неверной попытки оно удаляется, а следующая проверка получит 410. Выдача расходует отдельную от 429-й суточную квоту на автора (не ту, что привела к 429) — админ-настраиваемый потолок задач в сутки; при его превышении 429 «Слишком много проверок за сегодня». Если в админке не включён ни один тип игры — 503 «Проверка временно недоступна»

Параметры:

  • payload зависит от game_type: у anagram — { "scrambled": "…" } (перемешанные буквы загаданного слова), у quiz — { "question": "…", "options": ["…", "…", "…", "…"] } (4 варианта, порядок каждый раз перемешан, правильный не помечен)
  • attempts_left — стартовое число разрешённых неверных попыток (сейчас 3), а не текущий остаток: он не обновляется в ответах POST .../verify
  • expires_at — момент, после которого задание больше не примут (сейчас через 300 секунд после выдачи)
  • Идентичность автора для суточной квоты выдачи — та же логика, что у остальных публичных форм: Telegram (X-TG-Init-Data), иначе PWA Bearer, иначе IP

Запрос

curl -X POST "/api/v1/challenge"

Ответ

{
  "challenge_id": "9G3f...",
  "game_type": "anagram",
  "payload": { "scrambled": "ТИЛОНОМ" },
  "expires_at": "2026-09-01T10:20:00Z",
  "attempts_left": 3
}
POST/challenge/{challenge_id}/verify

Проверить решение задания. Совпадение снимает не свою квоту, а суточный счётчик записи — тот же, что дал 429 у публичных форм каталога, — но только для той же идентичности автора, что была у исходного запроса: Telegram (X-TG-Init-Data), иначе PWA Bearer, иначе IP. Присылать нужно те же заголовки, что были у запроса, упёршегося в лимит, иначе снимется чужой счётчик. Сброс сам ограничен отдельной суточной квотой на автора (по умолчанию 10, настраивается в админке) — при её исчерпании 429 «Лимит проверок на сегодня исчерпан». Неверное решение — 400 «Неверный ответ», попытка засчитывается в счётчик задания; после третьей неверной задание удаляется. Просроченное, неизвестное challenge_id или уже исчерпавшее попытки задание — 410 «Проверка истекла»

Параметры:

  • Тело: { "solution": "…" } — для anagram: разгаданное слово без учёта регистра; для quiz: точный текст выбранного варианта из options, без учёта пробелов по краям
  • challenge_id — из ответа POST /challenge
  • Успешная проверка не увеличивает суточный лимит записи навсегда, а обнуляет счётчик текущих суток для той же идентичности — новый лимит копится с нуля
  • Ответ (200): { "ok": true }

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"solution":"МОНОЛИТ"}' \
  "/api/v1/challenge/9G3f.../verify"

Ответ

{ "ok": true }
GET/services/hub

Данные главной каталога: счётчик, топ карточек, популярные теги и порядок категорий

Параметры:

  • top_limit (1–20, по умолчанию 6)
  • tag_limit (1–50, по умолчанию 15)
  • category_limit (1–50, по умолчанию 12)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/hub"

Ответ

{ "total": 214, "top": [ ... ], "tags": ["ремонт"], "categories": ["remont-pod-klyuch"] }
GET/services/recommended

Похожие карточки в той же категории — для блока рекомендаций

Параметры:

  • category — слаг категории
  • categories — несколько категорий через запятую, объединяются с category в один список поиска; если не задан ни category, ни categories — список пустой
  • limit (1–10, по умолчанию 3)
  • exclude — id карточек через запятую, которые нужно пропустить
  • city — не фильтрует выдачу: поднимает карточки с этой локацией в начало списка. Если совпадений нет, список не пустеет — возвращается обычный топ категории
  • exclude_provider_types — форматы исполнителя через запятую, которые в подборку не годятся (например, showroom, manufacturer — те, кто продаёт, а не выполняет работы)

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/services/recommended?category=remont-pod-klyuch&limit=3&city=Минск"

Ответ

[{ "id": "abc123", "name": "…", "rating_avg": 4.7, ... }]
GET/services/categories

Дерево категорий каталога. Родитель задаётся полем parent_slug

Параметры:

  • lang — язык подписи, по умолчанию ru; при отсутствии перевода отдаётся русский
  • stage — этап пути ремонта: before_deal | design | preparation | rough | finish | furnishing | moving_in. Порядок значений и есть порядок пути. С этим параметром приходят категории запрошенного этапа плюс сквозные (см. is_cross_stage)
  • в ответе stage — этап категории или null, если категория вне пути
  • в ответе is_cross_stage — категория покрывает несколько этапов сразу («под ключ», генподряд, технадзор) и потому попадает в выборку любого этапа

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/categories?stage=finish"

Ответ

[
  {
    "slug": "remont-pod-klyuch",
    "label": "Ремонт под ключ",
    "labels": { "ru": "Ремонт под ключ", "en": "Turnkey renovation" },
    "parent_slug": null,
    "sort_order": 10,
    "status": "approved",
    "stage": null,
    "is_cross_stage": true
  }
]
GET/services/tags

Подсказки по тегам с числом карточек — для автодополнения

Параметры:

  • q — фильтр по префиксу
  • limit (1–50, по умолчанию 20)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/tags?q=рем"

Ответ

[{ "tag": "ремонт", "count": 132 }]
GET/services/cities

Справочник городов каталога услуг с областью и слагом для адреса посадочной

Параметры:

  • q — фильтр по префиксу
  • limit (1–100, по умолчанию 20)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/cities?q=мин"

Ответ

[{ "name": "Минск", "region": "Минская область", "slug": "minsk" }]
GET/services/features

Справочник признаков услуги (коды для фильтра feature)

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/features"

Ответ

[{ "code": "smeta", "label": "Смета" }]
GET/services/facets

Значения фасетов для фильтров: типы объектов, форматы исполнителя, единицы цены

Запрос

curl -H "X-API-Key: $API_TOKEN" "/api/v1/services/facets"

Ответ

{
  "object_types": [{ "code": "flat", "label": "Квартира" }],
  "provider_types": [{ "code": "brigada", "label": "Бригада" }],
  "price_units": [{ "code": "sqm", "label": "за м²" }]
}
GET/services/attributes

Характеристики, применимые к категории, с допустимыми значениями. multi — можно ли выбрать несколько

Параметры:

  • category — обязателен

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/services/attributes?category=remont-pod-klyuch"

Ответ

[
  {
    "code": "material",
    "label": "Материал",
    "multi": true,
    "values": [{ "code": "gipsokarton", "label": "Гипсокартон" }]
  }
]
GET/services/filter-counts

Сколько карточек останется при добавлении каждого значения фильтра к текущей выборке. Принимает те же фильтры, что /services (кроме сортировки, цены, рейтинга и пагинации)

Параметры:

  • q, category, tag, country, city, business, verified, has_photo, has_reviews

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/services/filter-counts?category=remont-pod-klyuch"

Ответ

{
  "object_types": { "flat": 120, "house": 44 },
  "provider_types": { "brigada": 64 },
  "features": { "smeta": 88 },
  "countries": { "BY": 214 }
}
GET/services/landing

Срез «категория × город» для посадочной страницы вместе с перелинковкой. Общестрановые карточки в срез не попадают. 404, если город или категория неизвестны

Параметры:

  • category — слаг категории, обязателен
  • city — слаг города латиницей (minsk), обязателен
  • lang — язык подписи категории, по умолчанию ru
  • в ответе price — ориентир цены среза: { currency, unit, min, median, max, sample_size } либо null. Считается по карточкам среза с ценой в паре «валюта + единица» — берётся самый представительный по числу карточек сегмент; медиана — по серединам ценовых вилок карточек. null, если в этом сегменте меньше 5 карточек с ценой

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/services/landing?category=remont-pod-klyuch&city=minsk"

Ответ

{
  "category": { "slug": "remont-pod-klyuch", "label": "Ремонт под ключ" },
  "city": "Минск",
  "city_slug": "minsk",
  "total": 47,
  "price": { "currency": "BYN", "unit": "sqm", "min": "18.00", "median": "26.50", "max": "45.00", "sample_size": 12 },
  "sibling_categories": [{ "slug": "otdelka", "label": "Отделка", "total": 31 }],
  "nearby_cities": [{ "slug": "brest", "name": "Брест", "total": 12 }]
}
GET/services/{id}/stats

Просмотры карточки услуги за период. null, если просмотров меньше порога (10)

Параметры:

  • period — период в днях (7–365, по умолчанию 30)

Запрос

curl "/api/v1/services/abc123/stats?period=30"

Ответ

{ "service_id": "abc123", "period_days": 30, "views": 210 }
POST/pending-developers

Заявка на подключение застройщика. Если сайт уже есть в системе — вернёт данные существующего застройщика вместо создания заявки; если заявка на этот сайт уже подана — вернёт её. Токен не нужен

Параметры:

  • Тело: { "name": "…", "website": "…", "notes"?: "…" }
  • Ограничения длины: name — 256, website — 512, notes — 500 символов

Запрос

curl -X POST -H "Content-Type: application/json" \
  -d '{"name":"ЖилБылСтрой","website":"https://hwb.by"}' \
  "/api/v1/pending-developers"

Ответ

{
  "id": "...",
  "name": "ЖилБылСтрой",
  "website": "https://hwb.by",
  "notes": null,
  "status": "pending",
  "created_at": "...",
  "updated_at": "..."
}
GET/registration-status

Все объекты с открытой регистрацией прямо сейчас (кэш 30 с, токен не требуется)

Запрос

curl "/api/v1/registration-status"

Ответ

[{ "developer_id": "ghb", "external_id": "OBJ-101", "registration_url": "..." }]
GET/events

История событий мониторинга

Параметры:

  • developer_id, object_id, event_type (OBJECT_APPEARED|OBJECT_REMOVED|OBJECT_UPDATED|REGISTRATION_OPENED|REGISTRATION_CLOSED|COMPLETION_SOON)
  • since, until (ISO datetime)
  • limit (макс. 500), page
  • COMPLETION_SOON — календарное, не diff-событие: дом сдаётся в ближайшие N дней, порог настраивается на направлении. Раз в день его получают дома, чей срок сдачи вошёл в окно; object_id у такого события — синтетический building:{id}, а не id объекта

Запрос

curl -H "X-API-Key: $API_TOKEN" \
  "/api/v1/events?developer_id=ghb&event_type=REGISTRATION_OPENED&limit=10"

Ответ

{
  "total": 42,
  "items": [{ "event_type": "REGISTRATION_OPENED", "occurred_at": "...", ... }]
}
GET/events/stream

SSE-стрим событий в реальном времени

Параметры:

  • developer_id — фильтр по застройщику
  • event_types — через запятую (OBJECT_APPEARED, OBJECT_REMOVED, OBJECT_UPDATED, REGISTRATION_OPENED, REGISTRATION_CLOSED, COMPLETION_SOON)
  • api_token — токен query-параметром (для EventSource)
  • Last-Event-ID — восстановление пропущенных событий

Запрос

curl -N -H "X-API-Key: $API_TOKEN" \
  "/api/v1/events/stream?developer_id=ghb&event_types=REGISTRATION_OPENED"

Ответ

event: REGISTRATION_OPENED
id: 1711234567890-0
data: {"external_id":"OBJ-101",...}

: keepalive
GET/field-configs

Метаданные публичных полей объекта: подписи и шаблоны текстов для событий появления/удаления/изменения поля. Кэш 5 минут

Запрос

curl "/api/v1/field-configs"

Ответ

[
  {
    "field_name": "price",
    "alias": "Цена",
    "description": null,
    "value_kind": "money",
    "empty_policy": "hide",
    "public_value_visibility": "visible",
    "appeared_template": "...",
    "removed_template": "...",
    "changed_template": "...",
    "updated_at": "..."
  }
]
GET/health

Статус сервиса и парсеров (токен не требуется)

Запрос

curl "/api/v1/health"

Ответ

{ "status": "ok", "version": "1.0.0", "developers": [...] }

SSE-стрим (Server-Sent Events)

Стрим позволяет получать события в реальном времени без постоянного опроса. Соединение остаётся открытым; сервер отправляет keepalive-пинги каждые 15 секунд. При разрыве браузер автоматически переподключается с заголовком Last-Event-ID, и сервер отдаёт все пропущенные события из буфера.

bash

# curl — события регистрации от застройщика ghb
curl -N -H "X-API-Key: $API_TOKEN" \
  "/api/v1/events/stream?developer_id=ghb&event_types=REGISTRATION_OPENED"

# С восстановлением пропущенных (Last-Event-ID)
curl -N -H "X-API-Key: $API_TOKEN" -H "Last-Event-ID: 1711234567000-0" \
  "/api/v1/events/stream?developer_id=ghb"

python

import httpx
import asyncio

async def stream_events():
    async with httpx.AsyncClient() as client:
        async with client.stream(
            "GET",
            "/api/v1/events/stream",
            headers={"X-API-Key": "sh_ваш_токен"},
            params={"developer_id": "ghb", "event_types": "REGISTRATION_OPENED"},
            timeout=None,
        ) as r:
            async for line in r.aiter_lines():
                if line.startswith("data:"):
                    import json
                    data = json.loads(line[5:].strip())
                    print(data)

asyncio.run(stream_events())

javascript

// Браузер или Node.js (EventSource API)
// EventSource не поддерживает заголовки —
// токен передаётся query-параметром api_token
const es = new EventSource(
  "/api/v1/events/stream?developer_id=ghb&api_token=sh_ваш_токен"
);

es.addEventListener("REGISTRATION_OPENED", (e) => {
  const data = JSON.parse(e.data);
  console.log("Открылась регистрация:", data.external_id);
});

es.onerror = () => {
  // Браузер автоматически переподключается
  console.log("Переподключение...");
};

go

package main

import (
  "bufio"
  "fmt"
  "net/http"
  "os"
  "strings"
)

func main() {
  req, _ := http.NewRequest("GET",
    "/api/v1/events/stream?developer_id=ghb", nil)
  req.Header.Set("X-API-Key", os.Getenv("API_TOKEN"))
  resp, _ := http.DefaultClient.Do(req)
  defer resp.Body.Close()

  scanner := bufio.NewScanner(resp.Body)
  for scanner.Scan() {
    line := scanner.Text()
    if strings.HasPrefix(line, "data:") {
      fmt.Println(strings.TrimPrefix(line, "data: "))
    }
  }
}

Схема данных объекта

ПолеТипОписание
idstringUUID объекта в stroi.homes
developer_idstringСлаг застройщика (например, ghb)
external_idstringУникальный ID объекта у застройщика
titlestringНазвание объекта
object_typeenumapartment | apartments | commercial | parking | storage
statusenumfor_sale | booked | sold | removed | rent
object_urlstringСсылка на объект на сайте застройщика
registration_openbooleanОткрыта ли онлайн-регистрация
registration_urlstring?Ссылка на форму регистрации
pricedecimal?Цена целиком, в валюте поля currency
price_per_sqmdecimal?Цена за м²
estimated_pricedecimal?Расчётная цена (area × price_per_sqm). Заполняется только когда price отсутствует
currencystringВалюта price, price_per_sqm и estimated_price. Заполнена всегда: либо валюта, заданная самому объекту, либо валюта его застройщика. Сортировка и фильтры по цене сравнивают не эту валюту, а BYN-эквивалент по последнему известному курсу НБРБ
has_price_historybooleanЕсть ли смысл запрашивать /objects/{object_id}/price-history. Надмножество: возможен true и при пустой серии
price_delta_pctdecimal?Последнее изменение цены в процентах, со знаком (−2.4 — подешевел). Считается только у объектов с настоящей ценой целиком; у расчётной estimated_price его нет. null, если цена ещё не менялась, у события изменения не сохранилось прежнее значение или изменение вышло нулевым
price_changed_atdatetime?Когда произошло это изменение цены. null там же, где null price_delta_pct
areadecimal?Площадь в м²
roomsint?Количество комнат (0 = студия)
floorint?Этаж
total_floorsint?Этажность дома
buildingstring?Корпус / секция
building_idstring?UUID дома, к которому отнесён объект; null, если дом определить не удалось
countrystring?Страна
regionstring?Область
districtstring?Район
citystring?Город
addressstring?Полный адрес строкой, как его отдаёт источник
streetstring?Улица — разобранная часть адреса. Может быть заполнена одновременно с address
housestring?Номер дома — разобранная часть адреса
quarterstring?Квартал / микрорайон — разобранная часть адреса
completion_datedate?Срок сдачи дома
sales_start_datedate?Дата старта продаж
registration_start_datedatetime?Дата и время старта онлайн-регистрации
offline_booking_datedate?Дата старта офлайн-бронирования
image_urlstring?Изображение объекта
thumb_urlstring?Уменьшенная копия изображения для списков. Может быть null, когда миниатюра ещё не готова — в этом случае используйте image_url
participantsarrayЗастройщики-соавторы: developer_id, name, role (contributor | attributed). Заполняется в карточке одного объекта и в плоском списке /developers/{id}/objects; в остальных выдачах со схемой объекта поле приходит пустым массивом
promotionsarray?Акции застройщика
extraobject?Дополнительные поля конкретного застройщика. Состав не фиксирован — не полагайтесь на него в интеграции
first_seen_atdatetimeДата первого обнаружения
updated_atdatetimeДата последнего обновления

Поля перечислены полностью. Машиночитаемая версия схемы — в OpenAPI-спецификации.

Changelog API

ВерсияДатаИзменения
v1.16.02026-09-11У дома и жилого комплекса появились свои ссылки для шеринга: POST /buildings/{building_id}/share-urls и POST /complexes/{complex_id}/share-urls — тело и форма ответа те же, что у объекта и услуги, каждая ссылка ведёт через собственный редирект /r/{token}, и переходы по ней считаются отдельно (#2171). В ответах /complexes и /buildings добавлено поле container_object_id — идентификатор карточки-контейнера, по которой у дома и комплекса теперь есть лента изменений (#2183). Рядом с container_prices появились container_external_id (у дома) и container_content.external_id (у комплекса): внешний идентификатор той же карточки, заполняется по тому же правилу — только когда у дома или комплекса нет видимых единиц продажи.
v1.15.02026-09-07У /buildings в ответе задокументированы registration_open, registration_url и registration_start_date — запись с карточки-контейнера дома: у застройщика, публикующего дом целиком, регистрация открывается на сам дом, а не на квартиру (#1957). До открытия флаг остаётся false и ссылки нет — видна только дата старта (#2001). У DELETE /services/{service_id} появилось необязательное тело { "reasons": [...], "comment": "…" } — причина, по которой карточку сняли; старый клиент, который его не шлёт, удаляет карточку как раньше (#1990). Появился новый тип события COMPLETION_SOON — дом сдаётся в ближайшие N дней, порог настраивается на направлении; событие доступно в истории /events и в подписке /events/stream (#1960)
v1.14.02026-09-06Ключ сущности в группе /reviews/{entity_type}/{entity_id} у застройщика принимает не только UUID, но и слаг — тот самый, что публичное API отдаёт в поле id; внутреннего идентификатора у клиента не было вовсе. Действует одинаково во всех восьми ручках группы, видимость застройщика проверяется как в остальном публичном API. У дома и материала ключ остаётся UUID
v1.13.02026-09-05Появилась группа полиморфных отзывов: GET /reviews/{entity_type}/{entity_id} — лента отзывов о материале, доме/объекте или застройщике вместе со средней оценкой (у объекта — по аспектам yard/noise/deadlines/finishing) и правом текущего пользователя оставить свой отзыв. Запись — POST/DELETE самого отзыва, POST .../reply (ответ представителя застройщика), POST/DELETE .../photos, PUT .../vote и POST .../report — как и у /developers/{id}/reviews, через обычный API-токен недоступна: пишет только аккаунт приложения при выполненном предметном условии
v1.12.02026-09-05Ломающее: /complexes теперь отдаёт конверт { total, page, page_size, items } вместо голого списка — та же форма, что у /objects и /objects/recent. Раньше ручка принимала page/page_size, но не отдавала total: узнать, есть ли следующая страница, было неоткуда
v1.11.02026-09-04У /complexes ЖК без единиц продажи, видимых домов и цены с карточки-контейнера теперь целиком исключён из выдачи — раньше такая пустая карточка всё равно приходила и открывала пустую страницу. Список buildings и счётчик buildings_count в /complexes/{complex_id} используют то же правило видимости домов, что и /buildings: дом без единиц продажи и без цены за метр на своей карточке-контейнере в список домов ЖК не попадает
v1.10.02026-09-03Появилась группа ручек правки каталога: GET и POST /catalog/{entity_type}/{entity_id}/suggestions и POST /catalog/{entity_type}/{entity_id}/reports — правки и жалобы по объектам, домам и застройщикам, каждая ждёт модератора. Изменилось поведение ленты /objects?group=building: дома, чью карточку источник наполнил ценой за м², снова приходят в неё — домом, а не объектом, и такой дом сворачивается даже в одиночку; под фильтрами rooms, min_price, max_price, min_area, max_area и registration_open они по-прежнему исчезают. В схеме объекта задокументированы currency, price_delta_pct и price_changed_at. У /buildings — параметр kind и то, что без него паркинги в выдачу не попадают; в сводке дома видны kind, completion_date и container_prices, в сводке комплекса — container_prices. У /developers в ответе задокументированы contacts и юрблок (legal_form, legal_name, unp, registered_year, legal_checked_at, is_claimed, is_verified), у /services/{service_id} и его POST/PATCH — свой юрблок. У /services/categories — параметр stage и поля stage и is_cross_stage. Задокументирован group=building у /developers/{id}/objects; у него же убрано упоминание несуществующего фильтра complex_id
v1.9.02026-09-01Появились /complexes и /complexes/{complex_id} — жилые комплексы со сводкой и списком домов, у /buildings в ответе complex_id и complex_name. У /objects новый фильтр complex_id. Ломающее для тех, кто читает выдачу целиком: дома и комплексы, отданные источником отдельной карточкой, больше не приходят в /objects, /objects/recent и /objects/suggestions и не считаются в total_count дома — по прямой ссылке такая карточка открывается по-прежнему
v1.8.02026-08-30Per-token rate limit 600 req/мин теперь действует и при токене в query-параметре ?api_token=, а не только в заголовке X-API-Key: интегратор с действующим токеном больше не рискует часовым баном по IP. Недействительный токен по-прежнему считается по общему лимиту на IP
v1.7.02026-08-26Задокументирован параметр city у /services/recommended: поднимает карточки с этой локацией, не фильтрует. В /buildings и /buildings/{building_id} видны поля street, house, quarter. Уточнено: per-token rate limit 600 req/мин действует только при токене в заголовке X-API-Key — токен query-параметром ?api_token= считается по общему лимиту на IP
v1.6.02026-08-24Задокументированы POST /services, PATCH /services/{service_id} и DELETE /services/{service_id}: создание карточки специалиста и прямая правка/удаление её автором
v1.5.02026-08-18Поле thumb_url у объекта и дома — уменьшенная копия изображения для списков. Поле participants в карточке одного объекта: застройщики-соавторы с ролью contributor или attributed. Параметр attribution=own|participated у /developers/{id}/objects — отдаёт чужие объекты, где застройщик указан соавтором
v1.4.02026-07-31Поле estimated_price в объекте (area × price_per_sqm, когда застройщик не публикует итоговую цену). Задокументированы сквозной каталог /objects, /objects/recent, /objects/cities, /objects/suggestions, POST /objects/batch и отзывы застройщика. Уточнено: токен обязателен только для /events/stream и POST /objects/batch
v1.3.02026-06-27GET /developers/{id}/destinations — целевые направления застройщика (Telegram, Threads, Viber) единым списком
v1.2.02026-06-04Обязательные API-токены для data-эндпоинтов (X-API-Key / Bearer / ?api_token=). Бесплатная выдача через POST /api/v1/developer-tokens. Per-token rate limit 600 req/мин, авто-отзыв за злоупотребления, удаление после 90 дней неактивности. /health и /registration-status — без токена
v1.1.02026-04-18Sandbox API (/api/v1/sandbox/*) — тестовая среда с моковыми данными и SSE-стримом для разработки интеграций
v1.0.02026-03-29Первый публичный релиз: /developers, /objects, /events, /events/stream, /registration-status, /health

Политика: минорные изменения (новые поля) — обратно совместимы. Мажорная смена версии — через новый префикс /api/v2/.