이 페이지의 내용
압축 해제된 확장 프로그램 설치
Google Maps scraper 확장 프로그램은 워크스페이스 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를 사용합니다. 소스 레코드에서 쓰기 권한을 추론하지 마십시오.
비즈니스 홈페이지 검사
열기 웹사이트 감사또는 저장된 레코드에서 이 웹사이트 검사를 선택합니다. 정규화된 홈페이지를 검토하고, 추정하고, 1단위를 승인합니다. 완료된 HTTP/HTML 관찰을 검사하고, 내보내고, 보고서를 생성하거나 반복 모니터링을 설정합니다. 대량 조사는 한 줄에 하나의 웹사이트도 허용합니다.
홈페이지 검사는 라이브 맵 데이터 소스를 사용하지 않습니다. 이 작업은 최대 256KiB를 읽고, 최대 5번의 안전한 리디렉션을 따르며 15초 후에 중지됩니다. JavaScript를 실행하거나 순위 점수를 계산하지 않습니다. 차단되거나 시간 초과된 관찰도 1단위를 소모합니다.
{"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 라이브러리로 job을 생성한 다음, 최종 상태를 기다렸다가 보관된 결과를 읽습니다:
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 | 오늘의 실시간 리서치 총량을 모두 사용했습니다. 다음 UTC 날짜까지 Retry-After에 표시된 초만큼 기다리세요. 아무것도 예약되지 않습니다. |
500 | INTERNAL_ERROR | 예기치 않은 오류가 발생했습니다. 나중에 같은 멱등성 키로 다시 시도하세요. |
503 | PROVIDER_NOT_CONFIGURED | 이 워크스페이스에 실시간 리서치가 열려 있지 않거나 작업의 데이터 소스가 구성되지 않았습니다. |
AI 검색 답변을 검사합니다.
AI 검색 가시성은 Google AI 개요 관찰 항목 하나를 유지합니다. 여기에는 질문, 국가, 언어, 선택적 도시 편향, 답변 텍스트 및 안전한 인용 링크가 포함됩니다. 최대 5개의 브랜드 이름과 도메인을 추가하고, 최대 단위 수(1개)를 확인한 후 작업을 명시적으로 승인하세요. 관리자는 작업 공간을 활성화하고 라이선스가 부여된 소스를 연결해야 합니다.
데이터 소스를 다시 호출하지 않고 기록을 검사하거나 증거를 내보낼 수 있습니다. 쿼리, 지역 및 대상이 일치하는 경우에만 기준선을 비교하세요. 개요가 누락된 경우 가시성을 알 수 없습니다. 인용 순서는 검색 순위가 아닙니다. 불완전한 증거로는 부정 일치를 설정할 수 없습니다. 다중 엔진 추적은 사용할 수 없습니다.
정해진 질문 세트를 추적하려면 인증된 소유자나 관리자가 반복 시리즈를 예약할 수 있습니다. 프롬프트는 최대 10개, 간격은 24~720시간이며 90일 이내 최대 30회 관측합니다. 전체 시리즈의 최대 단위를 검토하고 명시적으로 확인하세요. 승인이나 구성이 바뀌거나 관측이 실패하거나 불확실하면 시리즈가 일시 중지되며, 다시 시작하려면 새 미리보기가 필요합니다.
영역을 검색하고 더 많은 페이지 가져오기
탐색에서 텍스트 위치, 지점 편향, 반경 또는 경계 상자를 선택합니다. 지점 편향은 경계가 아닙니다. 반경 및 상자 결과는 외부 또는 사용할 수 없는 좌표를 제외하고 제외 횟수를 유지합니다. 작업 공간 설정에서 반경 및 상자 크기를 제어합니다. 반자오선을 가로지르는 상자를 분할합니다. 제한된 샘플은 완전한 커버리지를 보장하지 않습니다.
리뷰 및 사진 메타데이터는 소스 페이지당 최대 100개의 레코드를 허용합니다. 결과가 완료되면 탐색 또는 작업 내역에서 결과 페이지를 엽니다. 정확한 소스 옵션을 검토하고 권한 부여 확인란을 선택한 다음 추가 페이지를 가져오기 위한 예산을 설정합니다. 정렬, 언어, 번역 및 필터는 체인 동안 고정됩니다. 커서 누락 또는 중복, 만료, 구성된 제한 및 불확실한 결과는 계속 진행을 중단합니다.
다음 저장된 레코드 페이지를 읽거나 기존 페이지를 내보낼 때 데이터 소스를 호출하지 않습니다. 새 소스 페이지를 가져오는 것은 별도의 유료 작업입니다. SDK 및 에이전트의 경우 계속 진행 미리 보기를 읽은 다음 안정적인 멱등성 키와 명시적인 예산을 사용하여 정확한 작업을 제출합니다. 커서 계속 진행을 예약하거나 일괄 처리하지 마십시오.
다각형 내부에서 조사 계획 수립
정확한 다각형 필터링을 사용하여 경계가 지정된 직사각형 요청을 하려면 탐색에서 다각형 경계를 선택합니다. 대량 조사에서 다각형 경계를 선택하여 GeoJSON 다각형을 가져오거나 붙여넣고, 모양을 검사하고, 셀당 행, 열 및 레코드를 선택합니다. 도식은 좌표와 계획된 셀을 보여주는 것이며, 도로 지도가 아닙니다.
위치는 [경도, 위도] 형식을 사용합니다. 각 링을 첫 번째 위치를 반복하여 닫습니다. 첫 번째 링은 바깥쪽 가장자리이며 최대 4개의 추가 링은 제외된 구멍입니다. 자체 교차, 접촉 또는 겹치는 구멍, 반자오선 교차는 허용되지 않습니다. 날짜 변경선을 가로질러 별도의 다각형을 사용합니다. 작업 공간 설정은 경계 위치, 범위 및 계획된 셀을 지정합니다.
'셀 검토 및 총 예산'은 데이터 소스를 호출하지 않습니다. 유지되는 모든 셀을 검사하고 최대 단위를 설정한 후 실행하기 전에 명시적으로 확인합니다. 모든 작업은 함께 예약되거나 아무것도 예약되지 않습니다. 영수증은 원래 계획 및 구성 버전을 보존합니다. 원본 결과는 폴리곤과 셀 모두에 대해 필터링됩니다. 공유된 엣지로 인해 중복된 비즈니스가 생성될 수 있습니다. 요청된 레코드 수는 최대치이며 전체 데이터 수가 아닙니다.
에이전트는 정확한 압축 플랜, 미리 보기 해시, 확인, 안정 키 및 집계 예산을 사용하여 preview_polygon_plan 다음에 create_reviewed_polygon_batch를 사용합니다. 기존 배치 기록, 취소 및 작업별 결과/내보내기 제어를 사용하십시오. 불확실한 결과는 자동으로 다시 스캔되지 않습니다.
보존된 페이지 체인을 내보냅니다.
결과 페이지에서 여러 페이지 내보내기를 열고 전체 체인 내보내기 검토를 선택합니다. 선택한 작업을 통해 페이지를 검토하고 각 정확한 ID에 대한 첫 번째 레코드를 유지할지 또는 모든 레코드를 유지할지 선택한 다음 비공개 내보내기를 확인합니다. 이 작업은 새 데이터를 가져오거나 조사 단위를 사용하지 않습니다. 내보내기는 진행률, 취소 및 무결성 영수증과 함께 데이터 제어에도 표시됩니다.
완료 후 JSONL, JSON, CSV 또는 Excel 파일로 다운로드하세요. 내용이 변경된 중복 ID는 충돌로 간주되며, 첫 번째 레코드가 유지됩니다. ID가 없는 레코드는 별도로 보관됩니다. 각 소스 페이지에는 검증된 JSONL 파트가 있으므로 전송 실패 시 파트별로 재개할 수 있습니다. Excel은 최대 5MB의 저장된 파트를 지원하며, 더 큰 용량의 내보내기는 스트리밍 형식을 사용합니다. CSV 파일은 extra_fields_json에 추가 열을 유지합니다.
소유자 및 관리자는 설정에서 일일 요청, 보류 중인 내보내기, 페이지 및 소스 바이트 제한, 보존 기간을 구성합니다. 가장 빠른 소스 만료일이 항상 적용됩니다. JSON 또는 JSONL을 소비할 때는 전체 마커를 유지하고 다운로드한 부분을 SHA-256 리셉션과 비교하여 확인하십시오. 동결된 체인은 보존된 샘플일 뿐, 전체 시장 커버리지를 입증하는 것은 아닙니다.
인용된 증거를 사용하여 리뷰 주제를 분석합니다.
보고서에서 리뷰 통계 보고서로 시작하세요. 소유자 또는 관리자는 설정 및 사용량에서 AI 리뷰 분석을 활성화하고 일일 시도 횟수, 보류 중인 작업, 발췌 횟수 및 대기열 수명을 설정합니다. 기본적으로 비활성화되어 있습니다.
'분석 입력 검토'를 선택하여 한 번의 생성을 승인하기 전에 정확한 발췌문과 모델을 검사합니다. 보고서 순서에 따라 최대 40개의 서로 다른 발췌문과 10KB의 발췌문 텍스트가 선택됩니다. 한 번의 생성에는 자체 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 내보내기에서 용지와 방향을 선택한 다음 생성합니다. 기록에는 대기 중, 렌더링 중, 완료됨 및 중지된 요청이 표시되며 다운로드 또는 취소 컨트롤이 있습니다. 작업 공간 설정에서 활성화, 일일 요청, 보류 중인 요청 및 보존 기간을 제어합니다. 파일은 원본 파일과 함께 만료되거나 구성된 보존 기간이 만료될 때(둘 중 먼저 도래하는 시점) 만료됩니다. 렌더링에는 연구 단위가 소모되지 않습니다. 렌더링 실패 또는 불확실한 경우 명시적인 새 요청이 필요하며 자동으로 재시도되지 않습니다.
에이전트는 검토된 버전, 스냅샷 해시, confirmed_render 및 멱등성 키와 함께 generate_report_pdf를 사용합니다. 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, 마침표, 원본 본문을 이은 값의 16진수 HMAC-SHA256이 붙으며, 키는 whsec_ 시크릿 전체를 텍스트로 사용합니다. X-LocalOS-Key-ID로 시크릿을 선택하세요.
어느 방식이든 타임스탬프가 5분 이내인지 확인하고 서명은 상수 시간으로 비교하세요. 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가 아닌 모든 응답은 재시도되며, 리디렉션도 마찬가지로 재시도되고 절대 따라가지 않습니다. 최대 8번의 시도가 약 28시간에 걸쳐 진행됩니다. 5초, 5분, 30분, 2시간, 5시간 후, 그다음 10시간 간격으로 두 번이며, 수신자가 Retry-After를 보내면 그 이후(최대 하루)입니다. 410 Gone 응답을 받으면 대상이 일시 중지되고, 다시 시작할 때까지 대기 중인 전송이 취소됩니다. 시도와 오류는 통합에 표시됩니다. 대상을 일시 중지하거나 제거하면 대기 중인 영수증이 취소되며, 이미 승인된 요청은 10초 이내에 완료될 수 있습니다. 저장된 전송 영수증에는 업체 레코드, 검색어, 계정 자격 증명, 수신자 응답 본문이 포함되지 않습니다.
Google Sheets, Zapier, Make 또는 n8n으로 결과 보내기
이 레시피는 위의 서명된 웹훅과 REST API만 사용합니다. 전송에는 작업 ID, 상태, 청구된 단위, 결과 수가 포함되며 결과 행은 포함되지 않습니다. 전송은 신호로만 취급하세요. 여기서 data.job_id만 가져온 뒤 읽기 전용 키로 API를 통해 작업과 그 행을 읽습니다. 이 요청은 인증되므로 누군가 전송을 위조하더라도 행은 항상 내 워크스페이스에서 가져옵니다.
이 키는 ‘API 및 키’에서 ‘기존 결과 읽기’ 권한으로 만드세요. 작업과 내보내기는 읽을 수 있지만 유료 조사를 시작할 수는 없습니다. 각 레시피는 해당 도구의 일반 웹훅, 코드 및 HTTP 요청 기능을 사용하며, MapsScraper.dev는 이러한 도구용 자체 앱을 제공하지 않습니다.
각 레시피는 키를 Authorization: Bearer <key> 형태로 보내는 GET 요청을 두 번 합니다. 먼저 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, 요청마다 Header Auth 자격 증명에 키를 넣은 HTTP Request node, 그 사이에 상태를 확인하는 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에서 읽기 전용 API 키를 담은, 이름이 MAPSSCRAPER_API_KEY인 스크립트 속성을 추가하면 키가 셀과 코드에 들어가지 않습니다.
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로 생성된 감성 분석 보고서가 아닙니다. 사진은 메타데이터이며 라이선스가 부여된 이미지 다운로드가 아닙니다. 반복 검색은 스냅샷을 유지하고, 안정적인 ID를 비교하며, 작업 공간 받은 편지함 알림을 생성합니다. 서명된 웹훅 영수증은 선택 사항이며 작업 메타데이터만 포함합니다. CRM별 필드 동기화는 별도로 진행됩니다.
