本页内容
安装解压后的扩展程序
扩展通过您的工作区 API 连接。Chrome 和 Edge 应用商店尚未批准分发。
- 下载并解压扩展程序包。
- 在 Chrome 浏览器中,打开
chrome://extensions。 在 Edge 浏览器中,打开edge://extensions。 启用开发者模式,选择“加载解压后的扩展程序”,然后选择解压后的文件夹。 - 打开扩展程序并选择“通过 MapsScraper.dev 连接”。在打开的页面中登录,由工作区所有者或管理员批准;之后扩展程序会获得本次浏览器会话专用的密钥。您也可以粘贴在“API 与密钥”中创建、具有“读取并运行绑定任务”权限的工作区 API 密钥。
- 选择语言,使用当前的 Maps 搜索,设置记录数和单位上限,然后运行。只有预估在单位上限内时才会提交搜索。
- 当响应丢失时,使用“恢复原始提交”。 如果会话已结束,请在重新提交之前检查任务历史记录。
从一项实际任务开始
- 创建工作区并打开“查找商家”。
- 输入包含位置信息的查询,例如“德克萨斯州奥斯汀的牙医”。
- 选择记录限制和可选筛选条件。 筛选条件在检索后生效。
- 查看最大单元使用量。 数据丰富会增加预留上限。
- 运行、检查数据源、选择企业并保存到列表或导出为 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 状态 | 错误代码 | 详细信息 |
|---|---|---|
400 | VALIDATION_ERRORINVALID_JSONINVALID_TASKIDEMPOTENCY_KEY_REQUIRED | 请求正文、某个标头或参数缺失或无效。请先修正请求再重试。 |
400 | TASK_BUDGET_REQUIRED | 缺少 X-Max-Units,或其值不是 1 到 10000 之间的整数。 |
401 | UNAUTHORIZED | 登录或使用有效密钥。 |
403 | INSUFFICIENT_SCOPEWORKSPACE_ACCESS_REVOKED | 您的密钥或会员资格不允许此操作。 |
404 | NOT_FOUND | 此工作区中不存在该资源。 |
405 | METHOD_NOT_ALLOWED | MCP 仅接受 POST 请求。GET 和 DELETE 会返回此代码,并附带 Allow: POST 标头。 |
409 | IDEMPOTENCY_CONFLICT | 幂等密钥被重用于更改的请求。 |
409 | JOB_NOT_CANCELLABLE | 任务已发送到数据源或已结束,因此无法取消。details.status |
410 | RESULT_EXPIRED | 保留的结果已过期。请运行新任务重新获取。 |
410 | WORKSPACE_UNAVAILABLE | 该工作区正在清除,对它的请求会被拒绝。 |
422 | TASK_BUDGET_TOO_LOW | 预估超出了你的 X-Max-Units 预算。details.needed, details.max_units |
422 | INSUFFICIENT_BALANCE | 可用单位低于该任务的上限。details.needed, details.available |
422 | DAILY_BUDGET_EXCEEDED | 该任务将超出工作区的每日单位上限。details.needed, details.daily_limit, details.used_today |
422 | PENDING_LIMIT_REACHED | 排队中或运行中的任务已过多。details.pending, details.max_pending |
422 | WORKSPACE_LIMIT_OR_BALANCE_EXCEEDED | 可用单位或工作区任务上限不足。 |
423, 409 | WORKSPACE_DELETION_PENDING | 该工作区处于删除前的宽限期。更改会以 423 拒绝,但读取、导出、账号操作和只读 MCP 工具仍可使用。只有在任务受理期间开始删除时,才会出现带有此代码或 WORKSPACE_NOT_ACTIVE 的 409。 |
429 | RATE_LIMITED | 已达到速率限制。请等待 Retry-After 中指定的秒数后重试。 |
429 | RESEARCH_DAILY_LIMIT_REACHED | 今日实时调研总量已用完。请等待 Retry-After 中指定的秒数,直到下一个 UTC 日。不会预留任何单位。 |
500 | INTERNAL_ERROR | 发生意外错误。请稍后使用相同的幂等键重试。 |
503 | PROVIDER_NOT_CONFIGURED | 实时调研未向此工作区开放,或任务的数据源尚未配置。 |
检查 AI 搜索结果
AI 搜索可见性保留一项 Google AI 概览观察结果:问题、国家/地区、语言、可选的城市偏好、答案文本和安全引用链接。 最多可添加五个品牌名称和域名,请查看单单元最大限制,然后明确授权该任务。 您的管理员必须启用工作区并连接已授权的来源。
无需再次调用数据源即可查看历史记录或导出证据。 仅当查询、语言和目标匹配时才比较基线。 缺少概览意味着可见性未知;引用顺序并非搜索排名。 不完整的证据无法建立否定匹配。 不支持多引擎跟踪。
如需跟踪一组固定问题,经过验证的所有者或管理员可以安排定期系列:最多 10 个提示,每 24 至 720 小时一次,90 天内最多 30 次观测。请查看整个系列的最大单位数并明确确认。当授权或配置发生变化,或某次观测失败或不确定时,系列会暂停;恢复需要重新预览。
搜索区域并获取更多页面
在“探索”中,选择文本位置、点偏好、半径或边界框。 点偏好并非边界。 半径和框结果会排除外部或不可用的坐标,并保留排除计数。 工作区设置控制半径和框的大小。 分割跨越反子午线的方框;有界样本并不能证明完全覆盖。
评论和照片元数据允许每个来源页面最多包含 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 特定字段同步仍然独立进行。
