本頁內容
安裝解壓縮後的擴充程序
擴充功能透過您的工作區 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 特定欄位同步仍獨立進行。
