Документация по парсеру Google Maps. От запроса до экспорта.

От первого поиска до повторяемого рабочего процесса агента. Инструкции, границы и контракты API вы найдете здесь.

На этой странице
  1. Установка распакованного расширения
  2. Начните с одной реальной задачи
  3. Запишите данные обогащения обратно в сохраненный список.
  4. Проверьте главную страницу компании.
  5. REST API
  6. Создание ограниченного задания
  7. Распространенные ошибки
  8. Проверка ответа на поиск ИИ
  9. Найти область и загрузить дополнительные страницы
  10. Планирование исследования внутри полигона
  11. Экспорт сохраненной цепочки страниц
  12. Анализ тем отзывов с использованием цитируемых доказательств
  13. Подготовка и отправка отчета клиенту
  14. Создать сохраненный PDF-файл
  15. Получение подписанных квитанций о заданиях
  16. Отправка результатов в Google Таблицы, Zapier, Make или n8n
  17. Важные ограничения

Установка распакованного расширения

Расширение-парсер Google Maps подключается к API вашего рабочего пространства. Распространение в магазинах Chrome и Edge не одобрено.

  1. Загрузка и распаковка пакета расширения.
  2. В Chrome откройте chrome://extensions. В Edge откройте edge://extensions. Включите режим разработчика, выберите «Загрузить распакованное» и выберите распакованную папку.
  3. Откройте расширение и выберите «Подключить через MapsScraper.dev». Войдите на открывшейся странице, где владелец или администратор рабочего пространства одобрит подключение; затем расширение получит собственный ключ для этого сеанса браузера. Вместо этого можно вставить ключ API рабочего пространства с доступом «Чтение и выполнение ограниченных задач» из раздела «API и ключи».
  4. Выберите язык, используйте текущий поиск Maps, задайте лимиты записей и единиц и запустите. Поиск отправляется, только если его оценка укладывается в лимит единиц.
  5. Используйте функцию «Восстановить исходную отправку», если ответ потерян. Если сессия завершилась, проверьте историю задач перед повторной отправкой.
Загрузите распакованный ZIP-архив расширения.
Контракт OpenAPI 3.1 Документация содержит все реализованные маршруты рабочего пространства, пагинацию, явные бюджеты и схемы ответов. Изучите... каталог функций для каждой страницы рабочей области.

Начните с одной реальной задачи

  1. Создайте рабочее пространство и откройте «Поиск компаний».
  2. Введите запрос с указанием местоположения, например, «Стоматологи в Остине, штат Техас».
  3. Выберите ограничение на количество записей и дополнительные фильтры. Фильтры применяются после получения данных.
  4. Проверьте максимальное использование единиц. Обогащение данных увеличивает зарезервированный лимит.
  5. Запустите, проверьте источники, выберите компании и сохраните в список или экспортируйте в CSV.

Запишите данные обогащения обратно в сохраненный список.

В разделе «Сохраненные списки» выберите записи с идентификатором источника и авторизуйте «Обогащение выбранных данных» с максимальным бюджетом единиц. «Проверить пакетную запись» открывает новый пакет; ожидающие задачи остаются пропущенными до более позднего предварительного просмотра. Вы также можете выбрать существующий пакет сведений или обогащения на панели инструментов списка.

Предварительный просмотр точных запрошенных идентификаторов и разверните каждое сравнение «до/после». Заполнение пустых полей сохраняет существующие значения; замена выбранных полей копирует только непустые поля источника, которые вы выбрали. Просмотрите дублирующиеся, несовпадающие, просроченные или остановленные задачи в списке пропущенных задач. Подтвердите отображаемое количество измененных записей и пропущенных задач перед применением. Для измененной записи, источника или конфигурации требуется новый предварительный просмотр.

Все подходящие обновления фиксируются одновременно. Этапы, заметки, теги, назначения и пользовательские поля остаются неизменными. История пакетной записи показывает исходные значения и возможность отмены для каждой записи; последующие изменения блокируют восстановление. Настройки рабочей области управляют включением и ограничениями на количество записей. Запись сохраненных результатов не использует единицы исследования и не обращается к источнику данных. Неразрешенные импорты по-прежнему используют индивидуальное «Сопоставление и слияние».

Агенты используют preview_batch_merge, apply_reviewed_batch_merge и read_batch_merge_receipt с одинаковым хешем проверки, областью действия и требованиями идемпотентности. Никогда не выводите разрешение на запись из исходных записей.

Проверьте главную страницу компании.

Открыть Аудит веб-сайтов, или выберите «Проверить этот веб-сайт» из сохраненной записи. Просмотрите нормализованную главную страницу, оцените и авторизуйте одну единицу. Проверьте завершенное наблюдение HTTP/HTML, экспортируйте его, создайте отчет или настройте повторяющийся мониторинг. Массовый поиск также принимает один веб-сайт на строку.

Проверка главной страницы не использует источник данных Maps в реальном времени. Она считывает максимум 256 KiB, отслеживает до пяти безопасных перенаправлений и останавливается через 15 секунд. Она не запускает JavaScript и не вычисляет рейтинговый балл. Заблокированное или истекшее по времени наблюдение по-прежнему потребляет одну единицу.

{"kind":"website_audit","url":"https://example.com/","limit":1}

REST API

GET      /api/health
GET      /api/me
GET      /api/v1/usage
POST     /api/v1/estimate
POST     /api/v1/jobs
GET      /api/v1/jobs/{id}
GET      /api/v1/jobs/{id}/results
POST     /api/v1/jobs/{id}/cancel
GET      /api/v1/jobs/{id}/export
GET/POST /api/v1/lists
GET/POST /api/v1/lists/{id}/items
PATCH    /api/v1/lists/{id}/items/{business_id}
GET/POST /api/monitors
PATCH    /api/monitors/{id}
POST     /mcp

Создание ограниченного задания

curl -X POST https://mapsscraper.dev/api/v1/jobs \
  -H "Authorization: Bearer $MAPSSCRAPER_API_KEY" \
  -H "Idempotency-Key: dentists-austin-tx-001" \
  -H "X-Max-Units: 1" \
  -H "Content-Type: application/json" \
  -d '{"kind":"search","query":"Dentists in Austin, TX","limit":20,"region":"us"}'

HTTP/1.1 202 Accepted
{"id":"6f1c2b3a-0000-4000-8000-000000000000","status":"queued","reserved":1}

Повторяйте ту же операцию с тем же ключом идемпотентности. Изменённый запрос с этим ключом возвращает 409. Задачи проходят статусы: в очереди, выполняется, завершена, частично, сбой, отменена или не определена. Неопределённый запрос к источнику данных требует сверки вместо автоматического платного повтора.

Большинство задач расходуют одну единицу. Обогащение контактов при поиске или снимке позиций резервирует ещё одну единицу на каждую запрошенную запись и списывает только за доставленные записи. Сведения о компании расходуют одну единицу, с контактами две; обогащение расходует две. Укажите X-Max-Units не меньше оценки.

В Python получите оценку задачи, создайте задание с помощью библиотеки requests, дождитесь финального статуса и прочитайте сохраненные результаты:

import os
import time
import uuid

import requests

API = "https://mapsscraper.dev/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MAPSSCRAPER_API_KEY']}"}
task = {"kind": "search", "query": "Dentists in Austin, TX", "limit": 20, "region": "us"}

# An estimate reserves nothing.
units = requests.post(f"{API}/estimate", json=task, headers=HEADERS, timeout=30).json()["units"]

# Keep the key: a retry of the same task must send it again.
key = str(uuid.uuid4())
job = requests.post(
    f"{API}/jobs",
    json=task,
    headers={**HEADERS, "Idempotency-Key": key, "X-Max-Units": str(units)},
    timeout=30,
)
job.raise_for_status()
job_id = job.json()["id"]

while True:
    status = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS, timeout=30).json()["status"]
    if status not in ("queued", "running"):
        break
    time.sleep(5)

if status in ("completed", "partial"):
    page = requests.get(
        f"{API}/jobs/{job_id}/results",
        params={"offset": 0, "limit": 100},
        headers=HEADERS,
        timeout=30,
    ).json()
    for business in page["items"]:  # follow page["next_offset"] for more
        print(business.get("name"), business.get("phone_number"), business.get("website"))

Распространенные ошибки

Каждый ответ с ошибкой представляет собой JSON со стабильным кодом, а некоторые содержат подробности. Ветвите логику по коду; несколько кодов могут иметь один HTTP-статус.

{"error":{"code":"TASK_BUDGET_TOO_LOW","details":{"needed":11,"max_units":5}}}
Статус HTTPКод ошибкиПодробности
400VALIDATION_ERRORINVALID_JSONINVALID_TASKIDEMPOTENCY_KEY_REQUIREDТело запроса, заголовок или параметр отсутствует или недопустим. Исправьте запрос перед повторной попыткой.
400TASK_BUDGET_REQUIREDX-Max-Units отсутствует или не является целым числом от 1 до 10000.
401UNAUTHORIZEDВойдите в систему или используйте действительный ключ.
403INSUFFICIENT_SCOPEWORKSPACE_ACCESS_REVOKEDВаш ключ или членство не позволяют выполнить это действие.
404NOT_FOUNDЭтот ресурс не существует в данном рабочем пространстве.
405METHOD_NOT_ALLOWEDMCP принимает только запросы POST. GET и DELETE возвращают этот код с заголовком Allow: POST.
409IDEMPOTENCY_CONFLICTКлюч идемпотентности был повторно использован для измененного запроса.
409JOB_NOT_CANCELLABLEЗадача уже отправлена в источник данных или завершена, поэтому её нельзя отменить.
details.status
410RESULT_EXPIREDСрок хранения результата истёк. Запустите новую задачу, чтобы получить его снова.
410WORKSPACE_UNAVAILABLEРабочее пространство удаляется. Запросы к нему отклоняются.
422TASK_BUDGET_TOO_LOWОценка превышает ваш бюджет X-Max-Units.
details.needed, details.max_units
422INSUFFICIENT_BALANCEДоступных единиц меньше, чем максимум задачи.
details.needed, details.available
422DAILY_BUDGET_EXCEEDEDЗадача превысит дневной лимит единиц рабочего пространства.
details.needed, details.daily_limit, details.used_today
422PENDING_LIMIT_REACHEDСлишком много задач уже в очереди или выполняется.
details.pending, details.max_pending
422WORKSPACE_LIMIT_OR_BALANCE_EXCEEDEDНедостаточно доступных единиц или исчерпан лимит задач рабочего пространства.
423, 409WORKSPACE_DELETION_PENDINGРабочее пространство находится в льготном периоде перед удалением. Изменения отклоняются с кодом 423, а чтение, экспорт, действия с аккаунтом и инструменты MCP только для чтения по-прежнему доступны. Ответ 409 с этим кодом или WORKSPACE_NOT_ACTIVE появляется, только если удаление начинается во время приёма задачи.
429RATE_LIMITEDДостигнут лимит запросов. Подождите столько секунд, сколько указано в Retry-After, затем повторите.
429RESEARCH_DAILY_LIMIT_REACHEDСегодняшний общий лимит онлайн-исследований исчерпан. Подождите столько секунд, сколько указано в Retry-After, до следующего дня по UTC. Ничего не резервируется.
500INTERNAL_ERRORПроизошла непредвиденная ошибка. Повторите позже с тем же ключом идемпотентности.
503PROVIDER_NOT_CONFIGUREDОнлайн-исследования не открыты для этого рабочего пространства, или источник данных задачи не настроен.

Проверка ответа на поиск ИИ

Видимость поиска ИИ сохраняет одно наблюдение в обзоре Google AI: вопрос, страна, язык, необязательный город, текст ответа и безопасные ссылки на цитирование. Добавьте до пяти названий брендов и доменов, проверьте максимальное значение в одну единицу, затем явно авторизуйте задачу. Ваш администратор должен включить рабочую область и подключить лицензированный источник.

Просмотрите историю или экспортируйте доказательства без повторного обращения к источнику данных. Сравнивайте базовые показатели только тогда, когда запрос, локаль и целевые объекты совпадают. Отсутствие обзора означает неизвестную видимость; порядок цитирования не является рейтингом поиска. Неполные данные не могут установить отрицательное совпадение. Отслеживание нескольких поисковых систем недоступно.

Чтобы отслеживать фиксированный набор вопросов, подтверждённый владелец или администратор может запланировать повторяющуюся серию: до 10 промптов, каждые 24–720 часов, не более 30 наблюдений за 90 дней. Проверьте максимум единиц для всей серии и явно подтвердите его. Серия приостанавливается, если меняются её авторизация или настройки либо наблюдение завершается сбоем или не определено; для возобновления нужен новый предварительный просмотр.

Открыть видимость поиска ИИ

Найти область и загрузить дополнительные страницы

В разделе «Исследование» выберите местоположение текста, точечное смещение, радиус или ограничивающую рамку. Точечное смещение не является границей. Результаты радиуса и рамки исключают внешние или непригодные координаты и сохраняют количество исключений. Настройки рабочей области управляют радиусом и размером рамки. Разделите прямоугольник, пересекающий антимеридиан; ограниченная выборка не доказывает полного покрытия.

Отзывы и метаданные фотографий позволяют обрабатывать до 100 записей на странице источника. После завершения обработки результатов откройте «Страницы результатов» в разделе «Исследование» или «История задач». Просмотрите точные параметры источника, установите флажок авторизации и задайте бюджет для получения еще одной страницы. Сортировка, язык, перевод и фильтры остаются неизменными для всей цепочки. Отсутствие или повторение курсоров, истечение срока действия, настроенные ограничения и неопределенные результаты останавливают продолжение.

Чтение следующей сохраненной страницы записи и экспорт существующей страницы не вызывают источник данных. Получение новой страницы источника является отдельной платной операцией. Для SDK и агентов ознакомьтесь с предварительным просмотром продолжения, затем отправьте точную задачу со стабильным ключом идемпотентности и явным бюджетом. Не планируйте и не обрабатывайте пакетно продолжения курсоров.

Планирование исследования внутри полигона

Выберите границу полигона в разделе «Исследование» для запроса одного ограниченного прямоугольника с точной фильтрацией полигонов. В разделе «Массовый поиск» выберите границу полигона, чтобы импортировать или вставить полигон GeoJSON, проверьте его форму и выберите количество строк, столбцов и записей на ячейку. Схема показывает координаты и запланированные ячейки; это не карта улиц.

Позиции используют [долгота, широта]. Замкните каждое кольцо, повторив его первую позицию; первое кольцо является внешним краем, и до четырех дополнительных колец являются исключенными отверстиями. Самопересечения, соприкасающиеся или перекрывающиеся отверстия и пересечения антимеридианов отклоняются. Используйте отдельные полигоны поперек линии смены дат. Настройки рабочей области ограничивают позиции, диапазон и запланированные ячейки.

«Проверка ячеек и общего бюджета» не вызывает источник данных. Проверьте каждую сохраненную ячейку, установите максимальное количество единиц и явно подтвердите перед запуском. Все задачи резервируются одновременно, или ни одна не принимается. Квитанции сохраняют исходный план и версию конфигурации. Исходный результат фильтруется как по полигону, так и по ячейке; общие ребра могут приводить к появлению дубликатов предприятий. Запрашиваемое количество записей является верхним пределом, а не полным подсчетом.

Агенты используют preview_polygon_plan, а затем create_reviewed_polygon_batch с точным компактным планом, предварительным хешем, подтверждением, стабильным ключом и совокупным бюджетом. Используйте существующую историю пакетов, отмену и управление результатами/экспортом для каждой задачи. Неопределенные результаты никогда не сканируются автоматически повторно.

Экспорт сохраненной цепочки страниц

В разделе «Страницы результатов» откройте «Экспорт нескольких страниц» и выберите «Обзор экспорта всей цепочки». Просмотрите страницы через выбранную задачу, выберите, следует ли сохранить первую запись для каждого точного ID или все записи, затем подтвердите частный экспорт. При этом не извлекаются новые данные и не используются единицы исследования. Экспорт также отображается в элементах управления данными с подтверждениями прогресса, отмены и целостности.

После завершения загрузите JSONL, JSON, CSV или Excel. Дублирующиеся идентификаторы с измененным содержимым считаются конфликтами; первая запись сохраняется. Записи без идентификаторов остаются отдельными. Каждая исходная страница имеет свою собственную проверенную часть JSONL, поэтому неудачная передача может быть возобновлена по частям. Excel поддерживает до 5 МБ сохраненных частей; для экспорта больших объемов используются потоковые форматы. CSV сохраняет дополнительные столбцы в extra_fields_json.

Владельцы и администраторы настраивают ежедневные запросы, ожидающие экспорты, ограничения на размер страниц и исходного кода, а также срок хранения в разделе «Настройки». Всегда применяется самый ранний срок действия исходного кода. Сохраняйте полный маркер при обработке JSON или JSONL и проверяйте загруженные части по их квитанциям SHA-256. Замороженная цепочка является сохраненным образцом, а не доказательством полного охвата рынка.

Анализ тем отзывов с использованием цитируемых доказательств

Начните с отчета по статистике отзывов в разделе «Отчеты». Владелец или администратор включает анализ отзывов с помощью ИИ в разделе «Настройки и использование» и устанавливает ежедневные попытки, ожидающие задачи, количество фрагментов и время жизни очереди. По умолчанию эта функция отключена.

Выберите «Проверка входных данных анализа», чтобы проверить точные фрагменты и модель перед разрешением генерации. В порядке отчета выбираются до 40 различных фрагментов и 10 KB текста фрагментов. У генерации есть собственные лимиты ИИ-анализа, и она не использует единицы кошелька Maps. Неудачные или отмененные попытки по-прежнему учитываются.

Каждая сохраненная тема включает проверенные цитаты и ссылки на источники. Подсчеты производятся на основе уникальных цитируемых фрагментов, темы могут перекрываться, а интерпретации ИИ могут быть неверными. Результат открывается как отдельный отчет: просмотрите его в разделе «Отчеты», затем выберите, чем поделиться или экспортировать в формате PDF. Скрытие столбца «Фрагмент отзыва» также скрывает анализ ИИ.

Фрагменты, не содержащие поддерживаемого пользовательского опыта, могут вернуть квитанцию с недостаточными доказательствами без отчета. Попытка по-прежнему учитывается в лимитах. Проверьте входные данные, прежде чем разрешить другой анализ. Обновление возможностей останавливает старые утверждения в очереди перед выводом; просмотрите новый предварительный просмотр. Существующие отчеты сохраняют свою исходную модель и версию запроса.

В квитанциях отображаются результаты в очереди, в процессе выполнения, завершенные, неудачные, неопределенные, отмененные или истекшие. Отмена предотвращает создание отчета, хотя отправленный анализ по-прежнему учитывается в лимитах. Неопределенные вызовы никогда не воспроизводятся автоматически. API, SDK и MCP используют один и тот же хеш предварительного просмотра, явное подтверждение и ключ идемпотентности.

Подготовка и отправка отчета клиенту

Создание отчета на основе завершенного исследования в разделе «Отчеты». Отредактируйте заголовок, фирменный стиль, порядок разделов и столбцы с доказательствами, затем сохраните презентацию. Настройки рабочей области предоставляют фирменные цвета по умолчанию, ограниченный логотип PNG, веб-сайт издателя и нижний колонтитул. Существующие сохраненные презентации изменяются только при их редактировании или применении настроек рабочей области по умолчанию.

Отправка проверенного отчета показывает точную публичную проекцию. Невыбранные поля с доказательствами и скрытые разделы исключаются из сохраненного снимка. Запросы к источникам по умолчанию являются приватными; методология остается видимой. Выберите любого пользователя со ссылкой или до 25 подтвержденных учетных записей электронной почты, установите срок действия и явно подтвердите контент и аудиторию. Вы также можете потребовать пароль для доступа; отправьте его отдельно от ссылки. Политика рабочей области может требовать пароли для каждого доступа к результатам/отчетам. Приглашения или электронные письма не отправляются.

Скопируйте полную ссылку один раз. Секретный ключ остается во фрагменте и не сохраняется в памяти браузера. Получатель с ограниченным доступом входит в систему с разрешенной подтвержденной учетной записью, затем открывает исходную ссылку и вводит пароль общего доступа по запросу. Пароли не могут быть восстановлены; если пароль потерян, его необходимо отозвать и создать новую ссылку. Срок действия ограничен сроком хранения источника. Управление данными может отозвать будущий доступ; удаленные отчеты, источники с истекшим сроком действия и приостановленные рабочие области останавливают новые операции чтения. Загруженные копии нельзя отозвать. JSON и печать/сохранение PDF в браузере используют отображаемое представление. Создание сохраненного PDF-файла создает частный файл из сохраненной версии на бумаге формата A4 или Letter с книжной или альбомной ориентацией. Завершенные файлы могут быть прикреплены к соответствующему проверенному общему ресурсу и наследовать его правила доступа.

Агенты используют preview_report_delivery, update_report_presentation, create_reviewed_report_share, list_report_shares и revoke_report_share. Для публикации требуется проверенная версия, хэш снимка, стабильный ключ идемпотентности и явное разрешение на совместное использование. Внешний исследовательский текст не может предоставить такое разрешение.

Создать сохраненный PDF-файл

Сохраните презентацию, выберите бумагу и ориентацию при экспорте в PDF, затем сгенерируйте. В истории отображаются запросы в очереди, в процессе рендеринга, завершенные и остановленные запросы с возможностью загрузки или отмены. Настройки рабочей области управляют включением, ежедневными запросами, ожидающими запросами и сроком хранения. Файлы удаляются вместе с их источниками или заданным периодом хранения, в зависимости от того, что наступит раньше. Рендеринг не расходует единицы исследования. Неудачные или неопределенные рендеринги требуют нового явного запроса; они не повторяются автоматически.

используют generate_report_pdf с проверенной версией, хэшем снимка, confirmed_render и ключом идемпотентности. list_report_pdfs и get_report_pdf проверяют квитанции; cancel_report_pdf останавливается до публикации. Функция Generate не создает общий доступ и не отправляет сообщение. Маршруты частной загрузки и прикрепления файлов для получателей находятся в OpenAPI.

Получение подписанных квитанций о заданиях

В разделе «Интеграции» добавьте общедоступный HTTPS-приемник. Надежно сохраните одноразовый секретный ключ подписи, включите доставку в разделе «Настройки и использование», а затем явно укажите место назначения. Выберите события завершенных, частичных, неудачных, неопределенных или отмененных заданий. Тесты отправляют только тестовый флаг и метаданные.

Каждая доставка содержит две схемы подписи. Для Standard Webhooks заголовок webhook-signature имеет вид v1, а затем base64 от HMAC-SHA256 по webhook-id, webhook-timestamp и точному исходному телу запроса, соединённым точками. Ключ: результат base64-декодирования текста после whsec_.

Исходная схема не изменилась: X-LocalOS-Signature имеет вид v1=, а затем шестнадцатеричный HMAC-SHA256 от X-LocalOS-Timestamp, точки и исходного тела, где ключ: весь секрет whsec_ как текст. Выбирайте секрет по X-LocalOS-Key-ID.

В обеих схемах требуйте метку времени не старше пяти минут и сравнивайте подписи за постоянное время. Идентификатор события в webhook-id и X-LocalOS-Event-ID не меняется между повторами: сохраняйте его как уникальный до применения изменений и подтверждайте дубликаты ответом 2xx.

Проверка подписи Standard Webhooks в Node.js:

// Verifies a webhook delivery with Web Crypto only, so it runs on
// Node.js 20+, Deno, Bun and Cloudflare Workers without dependencies.
//   const event = await verifyWebhook({ secret, headers, body });
// secret: "whsec_..." or, while rotating, ["whsec_new", "whsec_old"].
// headers: a Headers object or a plain map such as req.headers.
// body: the raw body as text or bytes, before any JSON parsing.
// Returns the parsed event. Otherwise throws an error whose code says
// why the delivery must not be trusted.
export class WebhookVerificationError extends Error {
  constructor(code) {
    super(code);
    this.name = "WebhookVerificationError";
    this.code = code;
  }
}

const encoder = new TextEncoder();

function header(headers, name) {
  if (typeof headers.get === "function") {
    return headers.get(name) ?? undefined;
  }
  for (const [key, value] of Object.entries(headers)) {
    if (key.toLowerCase() === name) {
      return Array.isArray(value) ? value[0] : value;
    }
  }
  return undefined;
}

function base64(value) {
  if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4) {
    return null;
  }
  try {
    return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
  } catch {
    return null;
  }
}

// Checks every byte, so the time taken does not reveal a mismatch.
function sameBytes(a, b) {
  if (a.length !== b.length) return false;
  let difference = 0;
  for (let i = 0; i < a.length; i++) difference |= a[i] ^ b[i];
  return difference === 0;
}

export async function verifyWebhook({
  secret,
  headers,
  body,
  toleranceSeconds = 300,
}) {
  if (!Number.isFinite(toleranceSeconds) || toleranceSeconds < 0) {
    throw new RangeError("toleranceSeconds must be 0 or more");
  }
  const fail = (code) => new WebhookVerificationError(code);
  const keys = [secret]
    .flat()
    .map((value) =>
      typeof value === "string" ? base64(value.replace(/^whsec_/, "")) : null,
    );
  if (!keys.length || keys.some((key) => !key?.length)) {
    throw fail("WEBHOOK_SECRET_INVALID");
  }
  const id = header(headers, "webhook-id");
  const timestamp = header(headers, "webhook-timestamp");
  const signatures = header(headers, "webhook-signature");
  if (!id || !timestamp || !signatures) {
    throw fail("WEBHOOK_HEADER_MISSING");
  }
  if (!/^\d{1,12}$/.test(timestamp)) {
    throw fail("WEBHOOK_TIMESTAMP_INVALID");
  }
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
    throw fail("WEBHOOK_TIMESTAMP_OUT_OF_RANGE");
  }
  const subtle = globalThis.crypto?.subtle;
  if (!subtle) throw fail("WEBHOOK_CRYPTO_UNAVAILABLE");
  const raw =
    typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
  const prefix = encoder.encode(`${id}.${timestamp}.`);
  const signed = new Uint8Array(prefix.length + raw.length);
  signed.set(prefix);
  signed.set(raw, prefix.length);
  const expected = await Promise.all(
    keys.map(async (key) => {
      const hmac = await subtle.importKey(
        "raw",
        key,
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["sign"],
      );
      return new Uint8Array(await subtle.sign("HMAC", hmac, signed));
    }),
  );
  // Any listed v1 signature may match; at most 16 are checked.
  const verified = signatures
    .split(" ")
    .filter(Boolean)
    .slice(0, 16)
    .some((entry) => {
      const [version, value] = entry.split(",", 2);
      const given = version === "v1" && value ? base64(value) : null;
      return given !== null && expected.some((d) => sameBytes(given, d));
    });
  if (!verified) throw fail("WEBHOOK_SIGNATURE_INVALID");
  try {
    return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
  } catch {
    throw fail("WEBHOOK_PAYLOAD_INVALID");
  }
}

Доставка выполняется как минимум один раз. Сбои сети, тайм-ауты и любой ответ вне 2xx повторяются, включая перенаправления, по которым переход никогда не выполняется. До восьми попыток распределяются примерно на 28 часов: через 5 секунд, 5 минут, 30 минут, 2 часа, 5 часов, затем дважды через 10 часов, либо позже, если получатель отправляет Retry-After (до одних суток). Ответ 410 Gone приостанавливает получателя и отменяет ожидающие доставки, пока вы его не возобновите. Попытки и ошибки отображаются в разделе «Интеграции». Приостановка или удаление получателя отменяет ожидающие квитанции; уже принятые запросы могут завершиться в течение десяти секунд. Сохранённые квитанции доставки не содержат записей о компаниях, поисковых запросов, учётных данных аккаунтов и тел ответов получателя.

Отправка результатов в Google Таблицы, Zapier, Make или n8n

Эти рецепты используют только описанные выше подписанные веб-хуки и REST API. Доставка содержит ID задачи, статус, списанные единицы и число результатов, но не строки результатов. Считайте её только сигналом: возьмите из неё data.job_id, а затем прочитайте задачу и её строки через API с ключом только для чтения. Эти запросы аутентифицированы, поэтому строки всегда берутся из вашего рабочего пространства, даже если кто-то подделает доставку.

Создайте этот ключ в разделе «API и ключи» с доступом «Чтение существующих результатов». Он может читать задачи и экспорты, но не может запускать платные исследования. Каждый рецепт использует общие функции веб-хуков, кода и HTTP-запросов самого инструмента; у MapsScraper.dev нет собственного приложения в этих инструментах.

Каждый рецепт делает два GET-запроса с ключом, переданным как Authorization: Bearer <key>: сначала GET /api/v1/jobs/{job_id}, и только если его status равен completed или partial, затем GET /api/v1/jobs/{job_id}/export за строками.

Как работает доставка

  • Ответьте любым статусом 2xx в течение 10 секунд, а медленную работу выполняйте потом.
  • Повторы используют тот же заголовок webhook-id, поэтому сохраняйте его и пропускайте уже обработанные доставки.
  • Неудачная доставка повторяется: до 8 попыток примерно за 28 ч или позже, если получатель отправит Retry-After. Перенаправления не выполняются.
  • Ответ 410 Gone приостанавливает получателя и отменяет ожидающие доставки. Исправьте приемник, затем снова включите получателя.

n8n

Используйте Webhook node с немедленным ответом, HTTP Request node для каждого запроса с ключом в учетных данных Header Auth и IF node между ними, который проверяет статус. Этот рецепт не проверяет подпись, а полагается на аутентифицированные запросы.

Zapier

Триггер Catch Raw Hook в Zapier передает неразобранное тело вместе с заголовками, поэтому этот рецепт также проверяет подпись. Добавьте шаг Code by Zapier, который выполняет JavaScript, и задайте его Input Data: body как исходное тело, id, timestamp и signature как заголовки webhook-id, webhook-timestamp и webhook-signature, а secret как секрет подписи. Любой, кто может редактировать Zap, видит этот секрет.

const crypto = require("crypto");
const { body, id, timestamp, signature, secret } = inputData;
const fresh =
  /^\d+$/.test(timestamp || "") &&
  Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
const key = Buffer.from(
  String(secret).replace(/^whsec_/, ""),
  "base64",
);
const expected = crypto
  .createHmac("sha256", key)
  .update(`${id}.${timestamp}.${body}`)
  .digest();
const signed = String(signature || "")
  .split(" ")
  .some((entry) => {
    const [version, value = ""] = entry.split(",");
    const given = Buffer.from(value, "base64");
    return (
      version === "v1" &&
      given.length === expected.length &&
      crypto.timingSafeEqual(given, expected)
    );
  });
const valid = Boolean(id) && fresh && signed;
if (!valid) return { valid };
return { valid, job_id: JSON.parse(body).data.job_id };

Добавьте шаг фильтра, который продолжает выполнение, только если valid равно true, затем выполните оба запроса шагами GET-запроса. Zapier отвечает на доставку кодом 200 независимо от результата проверки, поэтому доставку, не прошедшую проверку, останавливает фильтр, а не ответ.

Make

Используйте модуль Custom webhook, который превращает JSON-тело в поля, модуль HTTP для каждого запроса и фильтр между ними, проверяющий статус. Make сам отвечает на доставку кодом 200. Как и рецепт для n8n, этот рецепт не проверяет подпись и вместо этого полагается на аутентифицированные запросы.

Google Sheets

Google Sheets сам запрашивает строки, а не получает их push-доставкой. В таблице откройте Extensions, затем Apps Script, и вставьте этот скрипт. В Project Settings добавьте свойство скрипта с именем MAPSSCRAPER_API_KEY, содержащее ключ API только для чтения, чтобы ключ не хранился ни в ячейках, ни в коде.

const API = "https://mapsscraper.dev/api/v1";

function getJson(path) {
  const key = PropertiesService.getScriptProperties()
    .getProperty("MAPSSCRAPER_API_KEY");
  if (!key) throw new Error("Add the MAPSSCRAPER_API_KEY script property.");
  const response = UrlFetchApp.fetch(API + path, {
    headers: { Authorization: "Bearer " + key },
    muteHttpExceptions: true,
  });
  const status = response.getResponseCode();
  const text = response.getContentText();
  if (status !== 200) {
    throw new Error("HTTP " + status + ": " + text.slice(0, 200));
  }
  return JSON.parse(text);
}

// Text that starts with =, +, - or @ gets an apostrophe,
// so Sheets keeps it as text instead of running a formula.
function cell(value) {
  if (value === null || value === undefined) return "";
  if (typeof value === "object") value = JSON.stringify(value);
  const formula = typeof value === "string" && /^[=+\-@]/.test(value);
  return formula ? "'" + value : value;
}

function importLatestSearch() {
  const jobs = getJson("/jobs?page_size=25&kind=search").items;
  const job = jobs.find(
    (item) => item.status === "completed" || item.status === "partial",
  );
  if (!job) return;
  const path = "/jobs/" + job.id + "/export?format=json";
  const rows = getJson(path).items || [];
  if (!rows.length) return;
  const columns = [];
  rows.forEach((row) => Object.keys(row).forEach((name) => {
    if (!columns.includes(name)) columns.push(name);
  }));
  const values = [columns].concat(
    rows.map((row) => columns.map((name) => cell(row[name]))),
  );
  const book = SpreadsheetApp.getActive();
  const sheet =
    book.getSheetByName("MapsScraper") || book.insertSheet("MapsScraper");
  sheet.clearContents();
  sheet.getRange(1, 1, values.length, columns.length).setValues(values);
}

Запустите функцию importLatestSearch, чтобы скопировать последний завершенный поиск на лист с именем MapsScraper. Чтобы обновлять данные по расписанию, добавьте в Triggers триггер по времени для importLatestSearch. Apps Script останавливает выполнение через 6 минут и не может получить ответ больше 50 MB, поэтому читайте более крупные задачи постранично через GET /api/v1/jobs/{job_id}/results.

Важные ограничения

Отсутствие веб-сайта означает, что источник данных его не предоставил. Это не доказывает, что у компании нет веб-сайта. Снимки ранжирования представляют собой упорядочивание результатов одного источника данных, а не полную локальную SEO-сетку. Сбор отзывов не является отчетом об оценке настроения, сгенерированным ИИ. Фотографии являются метаданными, а не лицензированными загрузками изображений. Повторяющиеся поиски сохраняют снимки, сравнивают стабильные идентификаторы и создают уведомления в почтовом ящике рабочей области. Подписанные квитанции веб-перехватчика являются добровольными и содержат только метаданные задач; синхронизация полей, специфичных для CRM, остается отдельной.