Documentation du scraper Google Maps. De la requête à l’export.

De votre première recherche à un flux de travail d'agent reproductible. Retrouvez les instructions, les limites et les contrats API ici.

Sur cette page
  1. Installer l'extension décompressée
  2. Commencez par une vraie tâche
  3. Réécriture de l'enrichissement dans une liste enregistrée
  4. Inspecter la page d'accueil d'une entreprise
  5. REST API
  6. Créer un travail limité
  7. Erreurs courantes
  8. Inspecter une réponse de recherche AI
  9. Rechercher une zone et récupérer plus de pages
  10. Planifier une recherche à l'intérieur d'un polygone
  11. Exporter la chaîne de pages conservée
  12. Analyser les thèmes des avis avec les preuves citées
  13. Préparer et partager un rapport client
  14. Générer un PDF conservé
  15. Recevoir des reçus de travail signés
  16. Envoyer les résultats vers Google Sheets, Zapier, Make ou n8n
  17. Limites importantes

Installer l'extension décompressée

L'extension se connecte à l'API de votre espace de travail. La distribution dans les magasins Chrome et Edge n'est pas approuvée.

  1. Téléchargez et décompressez le package d'extension.
  2. Dans Chrome, ouvrez chrome://extensions. Dans Edge, ouvrez edge://extensions. Activez le mode développeur, choisissez Charger décompressé et sélectionnez le dossier décompressé.
  3. Ouvrez l’extension et choisissez « Se connecter avec MapsScraper.dev ». Connectez-vous sur la page qui s’ouvre, où un propriétaire ou un administrateur de l’espace approuve ; l’extension reçoit ensuite sa propre clé pour cette session du navigateur. Vous pouvez aussi coller une clé d’API d’espace avec l’accès « Lire et exécuter les tâches liées » depuis API et clés.
  4. Choisissez votre langue, utilisez votre recherche Maps actuelle, définissez les limites de fiches et d’unités, puis lancez. La recherche n’est envoyée que si son estimation respecte votre limite d’unités.
  5. Utiliser Récupérer la soumission originale lorsqu'une réponse est perdue. Si la session est terminée, vérifiez l'historique des tâches avant de la soumettre à nouveau.
Télécharger l'extension ZIP décompressée
Le Contrat OpenAPI 3.1 documente tous les itinéraires d'espace de travail implémentés, la pagination, les budgets explicites et les schémas de réponse. Explorez le répertoire de fonctionnalités pour chaque page d'espace de travail.

Commencez par une vraie tâche

  1. Créez un espace de travail et ouvrez Explorer les entreprises.
  2. Saisissez une requête avec son emplacement, par exemple « Dentistes à Austin, TX ».
  3. Choisissez votre limite d'enregistrements et vos filtres facultatifs. Les filtres s'appliquent après la récupération.
  4. Vérifiez l'utilisation maximale de l'unité. L'enrichissement augmente le plafond réservé.
  5. Exécutez, inspectez les sources, sélectionnez les entreprises et enregistrez dans une liste ou exportez au format CSV.

Réécriture de l'enrichissement dans une liste enregistrée

Dans « Listes enregistrées », sélectionnez les enregistrements qui ont un ID de source et autorisez « Enrichir la sélection » avec son budget maximal d'unités. « Vérifier la réécriture par lots » ouvre le nouveau lot ; les tâches en attente restent ignorées jusqu'à un aperçu ultérieur. Vous pouvez également choisir un lot de détails ou d'enrichissement existant dans la barre d'outils de la liste.

Prévisualisez les ID exacts demandés et développez chaque comparaison avant/après. Remplir les champs vides préserve les valeurs existantes ; Remplacer les champs sélectionnés copie uniquement les champs source non vides que vous choisissez. Passez en revue les tâches en double, sans correspondance, expirées ou arrêtées dans la liste ignorée. Confirmez le nombre d’enregistrements modifiés et de tâches ignorées affiché avant de postuler. Un enregistrement, une source ou une configuration modifié nécessite un nouvel aperçu.

Toutes les mises à jour éligibles sont validées ensemble. Les étapes, notes, balises, affectations et champs personnalisés restent intacts. L'historique des réécritures par lots affiche les valeurs d'origine et l'annulation par enregistrement ; les modifications ultérieures bloquent la restauration. Les paramètres de l’espace de travail contrôlent l’activation et les limites d’enregistrements. La réécriture des résultats conservés n'utilise aucune unité de recherche et ne fait pas appel à la source de données. Les importations non résolues utilisent toujours « Apparier et fusionner » individuellement.

Les agents utilisent preview_batch_merge, apply_reviewed_batch_merge et read_batch_merge_receipt avec les mêmes exigences de hachage de révision, de portée et d'idempotence. Ne jamais déduire l'autorisation d'écriture à partir des enregistrements sources.

Inspecter la page d'accueil d'une entreprise

Ouvrir Audits de sites Web, ou choisissez Inspecter ce site Web à partir d'un enregistrement enregistré. Consultez la page d'accueil normalisée, estimez et autorisez une unité. Inspectez l’observation HTTP/HTML terminée, exportez-la, créez un rapport ou définissez un moniteur récurrent. La Recherche groupée accepte également un site Web par ligne.

Une vérification de page d'accueil n'utilise pas la source de données Maps en direct. Elle lit au maximum 256 KiB, suit jusqu'à cinq redirections sécurisées et s'arrête après 15 secondes. Elle n'exécute pas JavaScript et ne calcule pas de score de classement. Une observation bloquée ou expirée consomme toujours une unité.

{"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

Créer un travail limité

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}

Relancez la même opération avec la même clé d’idempotence. Une requête modifiée avec cette clé renvoie 409. Les tâches passent par en file d’attente, en cours, terminée, partielle, échouée, annulée ou incertaine. Une requête incertaine vers la source de données exige un rapprochement plutôt qu’une relance payante automatique.

La plupart des tâches utilisent une unité. L’enrichissement des contacts sur une recherche ou un relevé de classement réserve une unité de plus par fiche demandée et ne facture que les fiches livrées. Les détails d’entreprise utilisent une unité, ou deux avec les contacts ; l’enrichissement en utilise deux. Réglez X-Max-Units au moins sur l’estimation.

En Python, estimez la tâche, créez le job avec la bibliothèque requests, attendez un statut final et lisez les résultats conservés :

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"))

Erreurs courantes

Chaque réponse d’erreur est un JSON avec un code stable, et certaines incluent des détails. Basez votre logique sur le code ; plusieurs codes peuvent partager le même statut HTTP.

{"error":{"code":"TASK_BUDGET_TOO_LOW","details":{"needed":11,"max_units":5}}}
Statut HTTPCode d’erreurDétails
400VALIDATION_ERRORINVALID_JSONINVALID_TASKIDEMPOTENCY_KEY_REQUIREDLe corps, un en-tête ou un paramètre est manquant ou invalide. Corrigez la requête avant de réessayer.
400TASK_BUDGET_REQUIREDX-Max-Units est absent ou n’est pas un nombre entier de 1 à 10000.
401UNAUTHORIZEDConnectez-vous ou utilisez une clé valide.
403INSUFFICIENT_SCOPEWORKSPACE_ACCESS_REVOKEDVotre clé ou votre adhésion ne permet pas cette action.
404NOT_FOUNDLa ressource n’existe pas dans cet espace de travail.
405METHOD_NOT_ALLOWEDMCP n’accepte que les requêtes POST. GET et DELETE renvoient ce code avec un en-tête Allow: POST.
409IDEMPOTENCY_CONFLICTUne clé d'idempotence a été réutilisée pour une demande modifiée.
409JOB_NOT_CANCELLABLELa tâche a déjà été envoyée à la source de données ou est terminée ; elle ne peut donc pas être annulée.
details.status
410RESULT_EXPIREDLe résultat conservé a expiré. Lancez une nouvelle tâche pour le récupérer à nouveau.
410WORKSPACE_UNAVAILABLEL’espace est en cours d’effacement. Les requêtes qui le visent sont refusées.
422TASK_BUDGET_TOO_LOWL’estimation dépasse votre budget X-Max-Units.
details.needed, details.max_units
422INSUFFICIENT_BALANCELes unités disponibles sont inférieures au maximum de la tâche.
details.needed, details.available
422DAILY_BUDGET_EXCEEDEDLa tâche dépasserait la limite quotidienne d’unités de l’espace de travail.
details.needed, details.daily_limit, details.used_today
422PENDING_LIMIT_REACHEDTrop de tâches sont déjà en file d’attente ou en cours.
details.pending, details.max_pending
422WORKSPACE_LIMIT_OR_BALANCE_EXCEEDEDLes unités disponibles ou une limite de tâches de l’espace sont insuffisantes.
423, 409WORKSPACE_DELETION_PENDINGL’espace est dans sa période de grâce avant suppression. Les modifications sont refusées avec 423, tandis que les lectures, exports, actions du compte et outils MCP en lecture seule restent possibles. Un 409 avec ce code ou WORKSPACE_NOT_ACTIVE n’apparaît que si la suppression commence pendant l’admission d’une tâche.
429RATE_LIMITEDUne limite de débit a été atteinte. Attendez le nombre de secondes indiqué dans Retry-After, puis réessayez.
429RESEARCH_DAILY_LIMIT_REACHEDLe quota de recherche en direct du jour est épuisé. Attendez le nombre de secondes indiqué dans Retry-After, jusqu’au jour UTC suivant. Rien n’est réservé.
500INTERNAL_ERRORUne erreur inattendue s’est produite. Réessayez plus tard avec la même clé d’idempotence.
503PROVIDER_NOT_CONFIGUREDLa recherche en direct n’est pas ouverte à cet espace de travail, ou la source de données de la tâche n’est pas configurée.

Inspecter une réponse de recherche AI

AI conserve une observation de Google AI Overview : la question, le pays, la langue, la préférence facultative pour la ville, le texte de la réponse et les liens de citation sécurisés. Ajoutez jusqu'à cinq noms de marque et domaines, vérifiez le maximum d'une unité, puis autorisez explicitement la tâche. Votre administrateur doit activer l'espace de travail et connecter une source sous licence.

Inspectez l’historique ou exportez les preuves sans nouvel appel à la source de données. Comparez les lignes de base uniquement lorsque la requête, les paramètres régionaux et les cibles correspondent. Un aperçu manquant signifie une visibilité inconnue ; l'ordre de citation n'est pas le classement de recherche. Des preuves incomplètes ne peuvent pas établir une correspondance négative. Le suivi multimoteur n’est pas disponible.

Pour suivre un ensemble fixe de questions, un propriétaire ou un administrateur vérifié peut planifier une série récurrente : jusqu’à 10 prompts, toutes les 24 à 720 heures, pour 30 observations au maximum sur 90 jours. Vérifiez le maximum d’unités de toute la série et confirmez-le explicitement. Une série se met en pause lorsque son autorisation ou sa configuration change, ou lorsqu’une observation échoue ou est incertaine ; la reprise exige un nouvel aperçu.

Visibilité de la recherche Open AI

Rechercher une zone et récupérer plus de pages

Dans Explorer, choisissez un emplacement de texte, un biais de point, un rayon ou un cadre de délimitation. Le biais ponctuel n’est pas une frontière. Les résultats de rayon et de boîte excluent les coordonnées extérieures ou inutilisables et conservent les décomptes d’exclusion. Les paramètres de l’espace de travail contrôlent le rayon et la taille de la boîte. Fendez une boîte qui traverse l'antiméridien ; un échantillon limité ne prouve pas une couverture complète.

Les métadonnées des avis et des photos autorisent jusqu'à 100 enregistrements par page source. Une fois le résultat obtenu, ouvrez Pages de résultats dans Explorer ou Historique des tâches. Vérifiez les options de source exactes, cochez la case d'autorisation et définissez un budget pour récupérer une page supplémentaire. Le tri, la langue, la traduction et les filtres restent fixes pour la chaîne. Les curseurs manquants ou répétés, l'expiration, les limites configurées et les résultats incertains arrêtent la poursuite.

La lecture de la page d'enregistrement enregistrée suivante et l'exportation d'une page existante n'appellent pas la source de données. La récupération d'une nouvelle page source est une opération payante distincte. Pour le SDK et les agents, lisez l'aperçu de la suite, puis soumettez sa tâche exacte avec une clé d'idempotence stable et un budget explicite. Ne planifiez pas et ne regroupez pas les continuations du curseur.

Planifier une recherche à l'intérieur d'un polygone

Choisissez Limite de polygone dans Explorer pour une requête de rectangle délimité avec filtrage exact des polygones. Dans Recherche groupée, choisissez Limite du polygone pour importer ou coller un polygone GeoJSON, inspecter sa forme et choisir des lignes, des colonnes et des enregistrements par cellule. Le schéma montre les coordonnées et les cellules planifiées ; ce n'est pas un plan des rues.

Les positions utilisent [longitude, latitude]. Fermez chaque anneau en répétant sa première position ; le premier anneau est le bord extérieur et jusqu'à quatre anneaux supplémentaires sont des trous exclus. Les auto-intersections, les trous se touchant ou se chevauchant et les croisements antiméridiens sont rejetés. Utilisez des polygones distincts sur la ligne de données. Paramètres de l'espace de travail, positions liées, étendues et cellules planifiées.

« Examiner les cellules Budget total » n'appelle pas la source de données. Inspectez chaque cellule conservée, définissez les unités maximales et confirmez explicitement avant d'exécuter. Toutes les tâches sont réservées ensemble ou aucune n'est admise. Les reçus conservent la version originale du plan et de la configuration. Un résultat source est filtré à la fois sur le polygone et sur la cellule ; les bords partagés peuvent produire des entreprises en double. Le nombre d’enregistrements demandé est un plafond et non un recensement.

Les agents utilisent preview_polygon_plan, puis create_reviewed_polygon_batch, avec le plan compact exact, le hachage de prévisualisation, la confirmation, la clé stable et le budget agrégé. Utiliser l'historique des lots existant, les contrôles d'annulation et d'exportation/résultat par tâche. Les résultats incertains ne sont jamais analysés à nouveau automatiquement.

Exporter la chaîne de pages conservée

Dans Pages de résultats, ouvrez Exportation multipage et choisissez Vérifier l'exportation complète. Parcourez les pages de la tâche sélectionnée, choisissez si vous souhaitez conserver le premier enregistrement pour chaque identifiant exact ou conserver tous les enregistrements, puis confirmez l'exportation privée. Cela ne récupère pas de nouvelles données et n'utilise pas d'unités de recherche. Les exportations apparaissent également dans les contrôles de données avec les reçus de progression, d'annulation et d'intégrité.

Télécharger JSONL, JSON, CSV ou Excel après l'exécution. Les ID dupliqués avec un contenu modifié sont comptabilisés comme des conflits ; le premier enregistrement est conservé. Les enregistrements sans ID restent séparés. Chaque page source possède sa propre partie JSONL vérifiée, ce qui permet de reprendre un transfert interrompu par une partie. Excel prend en charge jusqu'à 5 Mo de parties stockées. Les exportations volumineuses utilisent les formats de flux continu. Le format CSV conserve des colonnes supplémentaires dans extra_fields_json.

Les propriétaires et les administrateurs configurent les demandes quotidiennes, les exportations en attente, les limites d'octets de pages et de sources et la conservation dans les paramètres. La première expiration de la source s’applique toujours. Conservez le marqueur complet lorsque vous consommez du JSON ou du JSONL et vérifiez les pièces téléchargées par rapport à leurs reçus SHA-256. Une chaîne gelée est un échantillon conservé et non une preuve d’une couverture complète du marché.

Analyser les thèmes des avis avec les preuves citées

Commencez par un rapport de statistiques d'avis dans Rapports. Un propriétaire ou un administrateur active l'analyse des avis par l'IA dans Paramètres et utilisation et définit les tentatives quotidiennes, les tâches en attente, le nombre d'extraits et la durée de vie de la file d'attente. Il est désactivé par défaut.

Choisissez « Analyse des données d'entrée » pour inspecter les extraits exacts et le modèle avant d'autoriser une génération. Jusqu'à 40 extraits distincts et 10 KB de texte d'extrait sont sélectionnés dans l'ordre du rapport. Une génération a ses propres plafonds d'analyse IA et n'utilise pas d'unités de portefeuille Maps. Les tentatives échouées ou annulées comptent toujours.

Chaque thème enregistré comprend des citations et des références sources vérifiées. Les décomptes sont calculés à partir d’extraits cités distincts, les thèmes peuvent se chevaucher et les interprétations de l’IA peuvent être erronées. Le résultat s'ouvre sous la forme d'un rapport distinct : inspectez-le dans Rapports, puis choisissez les éléments à partager ou à exporter au format PDF. Le fait de masquer la colonne Extrait de l'avis masque également l’analyse IA.

Les extraits qui ne contiennent aucune expérience client prise en charge peuvent renvoyer un reçu avec des preuves insuffisantes sans rapport. La tentative est toujours prise en compte dans les limites. Inspectez l’entrée avant d’autoriser une analyse différente. Une mise à niveau de fonctionnalité arrête les anciennes approbations en file d'attente avant l'inférence ; revoir un nouvel aperçu. Les rapports existants conservent leur modèle d'origine et leur version d'invite.

Les reçus affichent des résultats en file d'attente, en cours d'exécution, terminés, en échec, incertains, annulés ou expirés. L'annulation empêche la création de rapports, même si une analyse envoyée est toujours prise en compte dans les limites. Les appels incertains ne sont jamais rejoués automatiquement. L'API, le SDK et le MCP utilisent le même hachage d'aperçu, la même confirmation explicite et la même clé d'idempotence.

Préparer et partager un rapport client

Créez un rapport à partir de la recherche terminée dans Rapports. Modifiez ses colonnes de titre, de marque, d’ordre des sections et de preuves, puis enregistrez la présentation. Les paramètres de l'espace de travail fournissent les couleurs de marque par défaut, un logo PNG délimité, un site Web d'éditeur et un pied de page. Les présentations enregistrées existantes ne changent que lorsque vous les modifiez ou appliquez les paramètres par défaut de l'espace de travail.

partages montre la projection publique exacte. Les champs de preuves non sélectionnés et les sections masquées sont exclus de l'instantané stocké. Les requêtes source sont privées par défaut ; la méthodologie reste visible. Choisissez toute personne disposant du lien ou jusqu'à 25 comptes de messagerie vérifiés, définissez une date d'expiration et confirmez explicitement le contenu et l'audience. Vous pouvez également exiger un mot de passe de partage ; envoyez-le séparément du lien. La stratégie d’espace de travail peut exiger des mots de passe sur chaque partage de résultats/rapports. Aucune invitation ni email n'est envoyé.

Copiez le lien complet une fois. Le secret reste dans son fragment et n'est pas conservé dans le stockage du navigateur. Un destinataire avec un accès restreint se connecte avec un compte vérifié autorisé, puis ouvre le lien d'origine et saisit le mot de passe de partage lorsque cela lui est demandé. Les mots de passe ne peuvent pas être récupérés ; révoquer et créer un lien de remplacement en cas de perte. L'expiration est limitée par la rétention de la source. Les contrôles des données peuvent révoquer les accès futurs ; les rapports supprimés, les sources expirées et les espaces de travail suspendus arrêtent les nouvelles lectures. Les copies téléchargées ne peuvent pas être rappelées. JSON et le navigateur Imprimer/Enregistrer PDF utilisent la présentation affichée. Générer un PDF enregistré crée un fichier privé à partir de la version enregistrée, avec du papier A4 ou Letter et une orientation portrait ou paysage. Les fichiers terminés peuvent être joints à un partage révisé correspondant et hériter de ses règles d'accès.

Les agents utilisent preview_report_delivery, update_report_presentation, create_reviewed_report_share, list_report_shares et revoke_report_share. La publication requiert la version révisée, le hachage de l'instantané, une clé d'idempotence stable et une autorisation de partage explicite. Les documents de recherche externes ne peuvent pas accorder cette autorisation.

Générer un PDF conservé

Enregistrez la présentation, choisissez le papier et l'orientation dans les exports PDF, puis générez. L'historique affiche les demandes en file d'attente, de rendu, terminées et arrêtées avec des contrôles de téléchargement ou d'annulation. Les paramètres de l'espace de travail contrôlent l'activation, les demandes quotidiennes, les demandes en attente et la conservation. Les fichiers expirent avec leurs sources ou la période de conservation configurée, selon la première éventualité. Le rendu ne dépense pas d'unités de recherche. Les rendus échoués ou incertains nécessitent une nouvelle demande explicite ; ils ne réessayent pas automatiquement.

utilisent generate_report_pdf avec la version révisée, le hachage de l'instantané, confirmed_render et une clé d'idempotence. list_report_pdfs et get_report_pdf vérifient les accusés de réception ; cancel_report_pdf interrompt la publication en attente. La génération ne crée pas de partage ni n'envoie de message. Les flux de téléchargement privé et de pièces jointes au destinataire sont disponibles via OpenAPI.

Recevoir des reçus de travail signés

Dans Intégrations, ajoutez un récepteur HTTPS public. Enregistrez son secret de signature unique en toute sécurité, activez la livraison dans Paramètres et paramètres. utilisation, puis activez explicitement la destination. Choisissez les événements de tâche terminés, partiels, échoués, incertains ou annulés. Les tests envoient uniquement un indicateur de test et des métadonnées.

Chaque livraison comporte deux schémas de signature. Pour Standard Webhooks, webhook-signature vaut v1, suivi du HMAC-SHA256 en base64 de webhook-id, webhook-timestamp et du corps brut exact de la requête, reliés par des points. La clé est le décodage base64 du texte qui suit whsec_.

Le schéma d’origine est inchangé : X-LocalOS-Signature vaut v1= suivi du HMAC-SHA256 hexadécimal de X-LocalOS-Timestamp, d’un point et du corps brut, avec pour clé le secret whsec_ entier sous forme de texte. Choisissez le secret avec X-LocalOS-Key-ID.

Avec l’un ou l’autre schéma, exigez un horodatage de moins de cinq minutes et comparez les signatures en temps constant. L’identifiant d’événement dans webhook-id et X-LocalOS-Event-ID reste le même d’une tentative à l’autre : enregistrez-le de façon unique avant d’appliquer des effets et accusez réception des doublons par une réponse 2xx.

Vérifier une signature Standard Webhooks dans 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");
  }
}

La livraison a lieu au moins une fois. Les échecs réseau, les délais dépassés et toute réponse hors 2xx sont relancés, y compris les redirections, qui ne sont jamais suivies. Jusqu’à huit tentatives sont réparties sur environ 28 heures : après 5 secondes, 5 minutes, 30 minutes, 2 heures, 5 heures, puis 10 heures deux fois, ou plus tard si le destinataire envoie Retry-After (jusqu’à un jour). Une réponse 410 Gone met la destination en pause et annule ses livraisons en attente jusqu’à ce que vous la repreniez. Les tentatives et les erreurs apparaissent dans Intégrations. Mettre en pause ou supprimer une destination annule les reçus en attente ; les requêtes déjà admises peuvent se terminer dans les dix secondes. Les reçus de livraison enregistrés ne contiennent aucune fiche d’entreprise, aucun terme de recherche, aucun identifiant de compte ni aucun corps de réponse du destinataire.

Envoyer les résultats vers Google Sheets, Zapier, Make ou n8n

Ces recettes n’utilisent que les webhooks signés décrits ci-dessus et la REST API. Une livraison contient l’ID de la tâche, le statut, les unités facturées et le nombre de résultats, mais pas les lignes de résultats. Traitez-la comme un simple signal : prenez-y data.job_id, puis lisez la tâche et ses lignes via l’API avec une clé en lecture seule. Ces requêtes sont authentifiées, donc les lignes proviennent toujours de votre propre espace de travail, même si quelqu’un falsifie une livraison.

Créez cette clé dans API et clés avec l’accès « Lire les résultats existants ». Elle peut lire les tâches et les exports, mais ne peut pas lancer de recherche payante. Chaque recette utilise les fonctions génériques de webhook, de code et de requête HTTP de l’outil ; MapsScraper.dev n’a pas d’application propre dans ces outils.

Chaque recette envoie deux requêtes GET avec la clé, transmise sous la forme Authorization: Bearer <key> : d’abord GET /api/v1/jobs/{job_id}, puis, seulement si son status vaut completed ou partial, GET /api/v1/jobs/{job_id}/export pour les lignes.

Fonctionnement de la livraison

  • Répondez avec n'importe quel statut 2xx dans les 10 secondes et effectuez les traitements lents ensuite.
  • Les nouvelles tentatives réutilisent le même en-tête webhook-id : enregistrez-le et ignorez les livraisons déjà traitées.
  • Une livraison échouée est retentée, jusqu'à 8 tentatives sur environ 28 heures, ou plus tard si votre récepteur envoie Retry-After. Les redirections ne sont pas suivies.
  • Une réponse 410 Gone met la destination en pause et annule ses livraisons en attente. Corrigez le récepteur, puis réactivez-la.

n8n

Utilisez un Webhook node réglé pour répondre immédiatement, un HTTP Request node pour chaque requête avec la clé dans un identifiant Header Auth, et entre les deux un IF node qui vérifie le statut. Cette recette ne vérifie pas la signature ; elle s’appuie plutôt sur les requêtes authentifiées.

Zapier

Le déclencheur Catch Raw Hook de Zapier transmet le corps non analysé et les en-têtes, donc cette recette vérifie aussi la signature. Ajoutez une étape Code by Zapier qui exécute du JavaScript et renseignez son Input Data : body avec le corps brut, id, timestamp et signature avec les en-têtes webhook-id, webhook-timestamp et webhook-signature, et secret avec le secret de signature. Toute personne qui peut modifier le Zap peut voir ce secret.

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 };

Ajoutez une étape de filtre qui ne continue que si valid est vrai, puis envoyez les deux requêtes avec des étapes de requête GET. Zapier répond à la livraison avec 200 que la vérification réussisse ou non : c’est donc le filtre, et non la réponse, qui arrête une livraison qui échoue.

Make

Utilisez un module Custom webhook, qui transforme le corps JSON en champs, un module HTTP pour chaque requête, et entre les deux un filtre qui vérifie le statut. Make répond lui-même à la livraison avec 200. Comme la recette n8n, celle-ci ne vérifie pas la signature et s’appuie plutôt sur les requêtes authentifiées.

Google Sheets

Sheets récupère les lignes au lieu de les recevoir par envoi. Dans votre feuille de calcul, ouvrez Extensions, puis Apps Script, et collez ce script. Dans Project Settings, ajoutez une propriété de script nommée MAPSSCRAPER_API_KEY contenant une clé API en lecture seule, afin que la clé reste en dehors des cellules et du code.

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);
}

Exécutez importLatestSearch pour copier la dernière recherche terminée dans une feuille nommée MapsScraper. Pour l’actualiser selon un calendrier, ajoutez un déclencheur temporel pour importLatestSearch dans Triggers. Apps Script arrête une exécution après 6 minutes et ne peut pas récupérer une réponse de plus de 50 Mo : lisez donc les tâches plus volumineuses page par page depuis GET /api/v1/jobs/{job_id}/results.

Limites importantes

Site Web manquant signifie que la source de données n'en a pas fourni. Cela ne prouve pas qu’une entreprise n’a pas de site Web. Les instantanés de classement sont un classement des résultats d'une source de données, et non une grille de référencement locale complète. La collecte d’avis n’est pas un rapport de sentiments généré par l’IA. Les photos sont des métadonnées et non des téléchargements d'images sous licence. Les recherches récurrentes conservent des instantanés, comparent les identités stables et créent des notifications dans la boîte de réception de l'espace de travail. Les reçus de webhook signés sont facultatifs et contiennent uniquement des métadonnées de tâche ; la synchronisation des champs spécifique à CRM reste séparée.