Google 地图抓取工具文档。 从查询到导出。

从您的首次搜索到可重复的代理工作流程。 在此查找说明、范围和 API 协议。

本页内容
  1. 安装解压后的扩展程序
  2. 从一项实际任务开始
  3. 将增强信息写回已保存的列表。
  4. 检查企业主页
  5. REST API
  6. 创建有界作业
  7. 常见错误
  8. 检查 AI 搜索结果
  9. 搜索区域并获取更多页面
  10. 在多边形内规划研究
  11. 导出保留的页面链
  12. 使用引用的证据分析评论主题
  13. 准备并共享客户报告
  14. 生成保留的 PDF
  15. 接收已签名的作业回执
  16. 将结果发送到 Google 表格、Zapier、Make 或 n8n
  17. 重要边界

安装解压后的扩展程序

扩展通过您的工作区 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 文件。

将增强信息写回已保存的列表。

在“已保存列表”中,选择具有来源 ID 的记录,并以其最大单位预算授权“丰富所选内容”。 “查看批量回写”会打开新批次;待处理任务将保持跳过状态,直到稍后预览。 您还可以从列表工具栏中选择现有的详细信息批次或增强批次。

预览请求的确切 ID,并展开每个前后对比。 填充空字段会保留现有值;替换选定字段仅复制您选择的非空源字段。 查看已跳过列表中的重复、不匹配、过期或已停止的任务。 应用之前,请确认显示的已更改记录和已跳过任务的数量。 已更改的记录、数据源或配置需要重新预览。

所有符合条件的更新将一起提交。 阶段、备注、标签、分配和自定义字段将保持不变。 批量回写历史记录显示原始值和每条记录的撤销操作;后续编辑会阻止恢复。 工作区设置控制启用和记录上限。 回写保留的结果不使用任何研究单元,也不会调用数据源。 未解决的导入仍然单独使用“匹配并合并”。

代理使用 preview_batch_merge、apply_reviewed_batch_merge 和 read_batch_merge_receipt,并具有相同的审核哈希、范围和幂等性要求。 切勿从源记录推断写入授权。

检查企业主页

打开 网站审核或从已保存的记录中选择“检查此网站”。 审核规范化的主页,估算并授权一个单元。 检查已完成的 HTTP/HTML 观察结果,将其导出,创建报告或设置定期监控。 批量研究也接受每行一个网站。

首页检查不使用实时地图数据源。 它最多读取 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。任务会经历排队中、运行中、已完成、部分完成、失败、已取消或不确定等状态。数据源请求结果不确定时需要对账,而不是自动付费重放。

大多数任务使用 1 个单位。在搜索或排名快照中补充联系人信息时,每条请求记录会额外预留 1 个单位,且只按实际交付的记录计费。商家详情使用 1 个单位,含联系人时为 2 个;信息补充使用 2 个。请将 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_REQUIRED缺少 X-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 工具仍可使用。只有在任务受理期间开始删除时,才会出现带有此代码或 WORKSPACE_NOT_ACTIVE 的 409。
429RATE_LIMITED已达到速率限制。请等待 Retry-After 中指定的秒数后重试。
429RESEARCH_DAILY_LIMIT_REACHED今日实时调研总量已用完。请等待 Retry-After 中指定的秒数,直到下一个 UTC 日。不会预留任何单位。
500INTERNAL_ERROR发生意外错误。请稍后使用相同的幂等键重试。
503PROVIDER_NOT_CONFIGURED实时调研未向此工作区开放,或任务的数据源尚未配置。

检查 AI 搜索结果

AI 搜索可见性保留一项 Google AI 概览观察结果:问题、国家/地区、语言、可选的城市偏好、答案文本和安全引用链接。 最多可添加五个品牌名称和域名,请查看单单元最大限制,然后明确授权该任务。 您的管理员必须启用工作区并连接已授权的来源。

无需再次调用数据源即可查看历史记录或导出证据。 仅当查询、语言和目标匹配时才比较基线。 缺少概览意味着可见性未知;引用顺序并非搜索排名。 不完整的证据无法建立否定匹配。 不支持多引擎跟踪。

如需跟踪一组固定问题,经过验证的所有者或管理员可以安排定期系列:最多 10 个提示,每 24 至 720 小时一次,90 天内最多 30 次观测。请查看整个系列的最大单位数并明确确认。当授权或配置发生变化,或某次观测失败或不确定时,系列会暂停;恢复需要重新预览。

Open AI 搜索可见性

搜索区域并获取更多页面

在“探索”中,选择文本位置、点偏好、半径或边界框。 点偏好并非边界。 半径和框结果会排除外部或不可用的坐标,并保留排除计数。 工作区设置控制半径和框的大小。 分割跨越反子午线的方框;有界样本并不能证明完全覆盖。

评论和照片元数据允许每个来源页面最多包含 100 条记录。 结果完成后,在“探索”或“任务历史记录”中打开“结果页”。 查看确切的源选项,勾选授权复选框,并设置预算以获取下一个页面。 排序、语言、翻译和筛选器在整个链中保持不变。 游标缺失或重复、过期、配置的限制以及不确定的结果都会停止继续执行。

读取下一个已保存记录页面和导出现有页面不会调用数据源。 获取新的来源页面是一项单独的付费操作。 对于 SDK 和代理,请阅读继续执行预览,然后使用稳定的幂等密钥和明确的预算提交其确切的任务。 请勿安排或批量执行游标继续执行。

在多边形内规划研究

在“探索”中选择“多边形边界”,即可对单个有界矩形请求进行精确的多边形筛选。 在“批量研究”中,选择“多边形边界”可导入或粘贴 GeoJSON 多边形,检查其形状,并选择每单元格的行数、列数和记录数。 示意图显示坐标和规划单元格;它不是街道地图。

位置使用 [经度,纬度]。 通过重复其第一个位置来闭合每个环;第一个环是外边缘,最多四个额外的环是排除的孔。 自相交、接触或重叠的孔以及反子午线交叉点将被拒绝。 跨越日期变更线时使用单独的多边形。 工作区设置可限定位置、跨度和规划单元格。

“审核单元格和总预算”不会调用数据源。 检查每个保留的单元格,设置最大单位并在运行前明确确认。 所有任务要么一起预留,要么一个都不接受。 收据保留原始计划和配置版本。 源结果会根据多边形和单元格进行过滤;共享边可能会导致重复的商家。 请求的记录数是上限,并非普查结果。

代理使用 preview_polygon_plan,然后使用 create_reviewed_polygon_batch,并具有完全相同的精简计划、预览哈希、确认、稳定密钥和汇总预算。 使用现有的批处理历史记录、取消和每个任务的结果/导出控制。 不确定的结果永远不会自动重新扫描。

导出保留的页面链

在“结果页”中,打开“多页导出”并选择“审核全链导出”。 通过所选任务查看页面,选择是保留每个精确 ID 的第一条记录还是保留所有记录,然后确认私有导出。 此操作不会获取新数据或使用研究单元。 导出内容也会显示在“数据控制”中,并带有进度、取消和完整性收据。

完成后,下载 JSONL、JSON、CSV 或 Excel 文件。 重复的 ID 且内容已更改将被视为冲突;保留第一个记录。 没有 ID 的记录保持独立。 每个源页面都有其自身的已验证 JSONL 部分,因此传输失败后可以分部分恢复。 Excel 支持最大 5 MB 的存储部分;更大的导出文件将使用流式格式。 CSV 会在 extra_fields_json 中保留额外的列。

所有者和管理员可在“设置”中配置每日请求数、待导出数据、页面和源字节限制以及保留期限。 始终采用最早的源过期时间。 使用 JSON 或 JSONL 时,请保留完整标记,并根据其 SHA-256 回执验证下载的部分。 冻结链是保留的样本,并非完整市场覆盖的证明。

使用引用的证据分析评论主题

首先在“报告”中查看评论统计报告。 所有者或管理员可在“设置”和“使用情况”中启用 AI 评论分析,并设置每日尝试次数、待处理任务、摘要计数和队列生命周期。 此功能默认关闭。

选择“审核分析输入”可在授权一次生成之前检查确切的摘录和模型。 按报告顺序最多选择 40 个不同的摘录和 10 KB 的摘录文本。 每次生成都有自己的 AI 分析上限,并且不使用 Maps 钱包单位。 失败或取消的尝试仍会计入。

每个已保存的主题都包含已核对的引文和来源参考。 计数基于不同的引用摘录计算,主题可能重叠,且 AI 解读可能出错。 结果将以单独报告的形式打开:在“报告”中查看,然后选择要共享的内容或导出为 PDF。 隐藏“评论摘录”列也会隐藏 AI 分析。

不包含受支持的客户体验的摘录可能会返回证据不足的收据,且不生成报告。 此尝试仍计入限制。 在授权进行其他分析之前,请检查输入内容。 功能升级会在推理之前停止旧的已排队审批;请查看新的预览。 现有报告保留其原始模型和提示版本。

收据显示已排队、正在运行、已完成、已失败、不确定、已取消或已过期的结果。 取消操作会阻止报告生成,但已派发的分析仍会计入限制。 不确定的调用永远不会自动重放。 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 用于阻止待发布的消息。 生成操作不会创建共享或发送消息。 私有下载和收件人附件路由采用 OpenAPI 实现。

接收已签名的作业回执

在“集成”中,添加一个公共 HTTPS 接收器。 安全地保存其一次性签名密钥,并在“设置”中启用交付。 使用后,请显式启用目标。 选择已完成、部分完成、失败、不确定或已取消的作业事件。 测试仅发送测试标志和元数据。

每次投递都带有两种签名方案。对于 Standard Webhooks,webhook-signature 的格式为 v1, 后接 webhook-id、webhook-timestamp 与原始请求正文用句点连接后计算的 base64 HMAC-SHA256。密钥是 whsec_ 之后文本的 base64 解码结果。

原有方案保持不变:X-LocalOS-Signature 的格式为 v1= 后接 X-LocalOS-Timestamp、一个句点和原始正文的十六进制 HMAC-SHA256,密钥为完整的 whsec_ 密钥文本。请根据 X-LocalOS-Key-ID 选择密钥。

无论采用哪种方案,都应要求时间戳在五分钟以内,并以恒定时间比较签名。webhook-id 和 X-LocalOS-Event-ID 中的事件 ID 在重试之间保持不变:在执行任何操作前以唯一方式保存它,并对重复投递返回 2xx 响应。

在 Node.js 中验证 Standard Webhooks 签名:

// 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

这些方案仅使用上文的签名 Webhook 和 REST API。每次投递包含任务 ID、状态、已计费单元数和结果数量,不包含结果行。请把投递仅当作信号:从中取出 data.job_id,再使用只读密钥通过 API 读取任务及其结果行。这些请求经过身份验证,因此即使有人伪造投递,结果行也始终来自你自己的工作区。

请在“API 和密钥”中创建该密钥,并授予“读取现有结果”权限。它可以读取任务和导出,但不能启动付费研究。每个方案都使用该工具的通用 Webhook、代码和 HTTP 请求功能;MapsScraper.dev 在这些工具中没有自己的应用。

每个方案都会用该密钥发出两个 GET 请求,密钥以 Authorization: Bearer <key> 形式发送:先请求 GET /api/v1/jobs/{job_id},仅当其 status 为 completed 或 partial 时,再请求 GET /api/v1/jobs/{job_id}/export 获取结果行。

投递的工作方式

  • 请在 10 秒内以任意 2xx 状态响应,耗时的工作之后再做。
  • 重试会沿用相同的 webhook-id 标头,因此请保存它并跳过已处理的投递。
  • 失败的投递会重试,在约 28 小时内最多 8 次;如果接收方发送 Retry-After,则会更晚重试。不会跟随重定向。
  • 收到 410 Gone 响应会暂停该目标并取消其待处理的投递。修复接收方后,再重新开启。

n8n

使用一个设置为立即响应的 Webhook node,每个请求各用一个 HTTP Request node 并把密钥放在 Header Auth 凭据中,两者之间再用一个 IF node 检查状态。此方案不验证签名,而是依靠经过身份验证的请求。

Zapier

Zapier 的 Catch Raw Hook 触发器会传递未解析的请求体和请求头,因此此方案还会验证签名。添加一个运行 JavaScript 的 Code by Zapier 步骤,并设置其 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

使用一个会把 JSON 请求体拆成字段的 Custom webhook 模块,每个请求各用一个 HTTP 模块,两者之间再用一个筛选器检查状态。Make 会自行以 200 响应投递。与 n8n 方案一样,此方案不验证签名,而是依靠经过身份验证的请求。

Google Sheets

Google Sheets 是主动拉取数据行,而不是接收推送。在电子表格中依次打开 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 排名网格。 评论收集并非 AI 生成的情感报告。 照片是元数据,并非授权图像下载。 重复搜索会保留快照,比较稳定的身份信息,并创建工作区收件箱通知。 已签名的 Webhook 收据为选择加入,仅包含任务元数据;CRM 特定字段同步仍然独立进行。