# GeoGridAPI · Documentation

> Une API REST et un serveur MCP pour les cartes de classement local. 1 crédit = 1 point de la grille.

Source : https://geogridapi.io/fr/docs

## Démarrage rapide

1\. Créez un compte et une clé de test (gga_test_) dans la console. 2. Envoyez une fiche Google, un mot-clé et le format de la carte à POST /v1/grids. 3. Lisez le résultat avec GET /v1/grids/{id} ou recevez-le par webhook.

**Créer une grille**

```bash
curl https://api.geogridapi.io/v1/grids \
  -H "Authorization: Bearer gga_test_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: plombier-vauban-2026-10" \
  -d '{
    "place_id": "ChIJ…",
    "keyword": "plombier",
    "size": 7,
    "radius_km": 3,
    "webhook_url": "https://example.com/webhooks/geogridapi"
  }'
```

L’API répond tout de suite 202 avec l’identifiant de la grille, statut pending. La mesure prend de 20 secondes à 2 minutes selon la taille.

**Lire une grille**

```bash
curl https://api.geogridapi.io/v1/grids/3f2c…  -H "Authorization: Bearer gga_test_…"
```

- [Tutoriel : un suivi de rang en ~20 lignes (en anglais)](https://geogridapi.io/en/guides/build-geo-grid-rank-tracker): Node.js ou Python : clé de test, attente du résultat ou webhook signé, carte en couleurs dans le terminal et image PNG.

## Authentification

Chaque requête porte une clé dans l’en-tête Authorization: Bearer. Les clés gga_live_ mesurent de vraies grilles et débitent vos crédits (1 crédit = 1 point de la grille). Les clés gga_test_ renvoient des grilles fictives, gratuites et déterministes, pour intégrer sans payer. Une clé n’est affichée qu’une fois, à sa création ; une clé révoquée cesse de fonctionner immédiatement.

## Créer une grille

POST /v1/grids. Indiquez soit place_id, soit query (nom et ville) : jamais les deux.

| Paramètre | Type | Description |
| --- | --- | --- |
| `place_id` | `string` | Identifiant Google de la fiche (ChIJ…). Trouvez-le avec GET /v1/places/search. |
| `query` | `string` | Nom et ville de la fiche ; la première correspondance est mesurée. |
| `center` | `{ lat, lng }` | Facultatif : centre de la grille. Évite la résolution de la fiche. |
| `keyword` | `string` | Mot-clé recherché sur Google Maps (1 à 120 caractères). |
| `shape` | `string` | Forme de la grille : square (par défaut), circle, diamond, hexagon, cross, ellipse ou rectangle. |
| `size` | `3 \| 5 \| 7 \| 9` | Carré seulement : côté de la grille, 7 par défaut. Coût : size × size points. Interdit avec une autre forme. |
| `density` | `low \| medium \| high` | Autres formes seulement : densité du treillis, medium par défaut. Points : cercle 13 / 29 / 49, losange 13 / 25 / 41, hexagone 17 / 27 / 47, croix 21 / 33 / 45, ellipse 19 / 35 / 45, rectangle 15 / 35 / 45. Interdit avec le carré. |
| `radius_km` | `number` | Distance du centre au bord : de 0,5 à 30 km pour le carré (1,5 / 2,5 / 3,5 / 4,5 km par défaut selon la taille), de 1 à 30 km pour les autres formes (par défaut, la moitié de la plus grande dimension du treillis, en km). |
| `language` | `string` | Langue de la recherche (code à 2 lettres). Par défaut : la langue du compte. |
| `country` | `string` | Pays de la recherche (code ISO à 2 lettres), facultatif. |
| `webhook_url` | `string` | URL HTTPS publique appelée à la fin de la grille. Par défaut : celle du compte. |

En-tête Idempotency-Key (facultatif) : une même clé renvoie la grille déjà créée, sans nouveau débit.

**Une grille en cercle**

```bash
curl https://api.geogridapi.io/v1/grids \
  -H "Authorization: Bearer gga_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Plomberie Vauban, Lille",
    "keyword": "plombier",
    "shape": "circle",
    "density": "medium",
    "radius_km": 3.5
  }'
```

Sur le littoral, les points qui tombent en mer ne sont ni mesurés ni débités : points_total peut être inférieur au compte de la forme.

## Prix exact avant de lancer

POST /v1/grids/estimate prend le même corps que POST /v1/grids et renvoie le nombre de points, les crédits et le solde après, sans rien réserver ni débiter. Carré : size × size points, sans appel au moteur. Autres formes : les points en mer sont retirés (points_at_sea), la fiche est donc placée comme pour une recherche (solde positif, 30 recherches par minute) ; sinon exact vaut false et points est une borne haute. Avec une clé de test, credits vaut 0.

**POST /v1/grids/estimate**

```bash
curl https://api.geogridapi.io/v1/grids/estimate \
  -H "Authorization: Bearer gga_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "query": "Plomberie Vauban, Lille", "keyword": "plombier", "shape": "circle" }'
```

**Réponse**

```json
{
  "object": "estimate",
  "shape": "circle", "density": "medium", "rows": 7, "cols": 7, "radius_km": 3.5,
  "points": 24, "points_at_sea": 5, "exact": true,
  "credits": 24, "balance": 9976, "balance_after": 9952, "enough": true,
  "place": { "place_id": "ChIJ…", "name": "Plomberie Vauban", "lat": 50.63, "lng": 3.06 }
}
```

## L’objet grille

ranks est une matrice rows × cols (size × size pour le carré) : rangée 0 au nord, colonne 0 à l’ouest. Chaque case donne la position de la fiche dans les résultats Google Maps à ce point ; null signifie absente du top 20. points détaille chaque point mesuré avec ses coordonnées et le top 3 des fiches.

Pour une forme autre que le carré, une case hors de la forme (ou en mer) vaut aussi null dans ranks : c’est points qui fait foi, une case absente de points n’a pas été mesurée. shape et density rappellent le format ; pour une forme, size vaut max(rows, cols).

summary : average_rank (moyenne sur les points où la fiche apparaît), average_rank_with_misses (absences comptées 21), top3_points, found_points.

**Réponse**

```json
{
  "id": "3f2c9b1e-…",
  "object": "grid",
  "status": "completed",
  "mode": "live",
  "place": { "place_id": "ChIJ…", "name": "Plomberie Vauban", "lat": 50.63, "lng": 3.06 },
  "keyword": "plombier",
  "shape": "square",
  "density": null,
  "size": 7,
  "rows": 7,
  "cols": 7,
  "radius_km": 3,
  "points_total": 49,
  "points_charged": 49,
  "summary": { "average_rank": 4.3, "average_rank_with_misses": 4.6, "top3_points": 28, "found_points": 48 },
  "ranks": [[4, 3, 2, 2, 3, 5, 8], …, [11, 8, 6, 5, 9, 15, null]],
  "points": [{ "row": 0, "col": 0, "lat": 50.657, "lng": 3.024, "rank": 4,
               "top": [{ "rank": 1, "place_id": "ChIJ…", "name": "…" }, …] }, …],
  "error": null,
  "created_at": "2026-10-16T10:00:00Z",
  "completed_at": "2026-10-16T10:00:41Z"
}
```

GET /v1/grids liste vos grilles, les plus récentes d’abord (limit jusqu’à 100, pagination par before = created_at du dernier élément), sans le détail des points. Filtres : place_id (une fiche) et keyword (mot-clé exact, sans tenir compte de la casse).

competitors : qui est devant. Les fiches du top 3 cumulées sur la carte, la plus présente d’abord : top3_points (points où elle est dans le top 3), first_points (points où elle est première), ahead_points (points où elle est classée devant votre fiche).

previous : l’écart avec la carte précédente de la même fiche (même mot-clé, format, rayon et centre), sinon null. changes.average_rank négatif = mieux ; rank_changes donne les places gagnées par case (positif = mieux, une absence compte 21). Présent dans GET /v1/grids/{id} et dans le webhook.

**Extrait**

```
"competitors": [
  { "place_id": "ChIJ…", "name": "Plomberie Express", "top3_points": 29, "first_points": 12, "ahead_points": 21 }
],
"previous": {
  "grid_id": "41d0…", "created_at": "2026-10-10T05:00:12Z",
  "average_rank": 7.2, "top3_points": 10, "found_points": 40,
  "changes": { "average_rank": -2.4, "top3_points": 14, "found_points": 8 },
  "ranks": [[…]], "rank_changes": [[2, 0, -1, …]],
  "points": { "improved": 27, "worsened": 6, "unchanged": 16 }
}
```

## L’image de la carte

GET /v1/grids/{id}/image renvoie l’image PNG d’une grille terminée (1200 × 1092) : la carte, les pastilles de rang, le mot-clé, la fiche, la date et les trois chiffres clés. lang=fr ou en (sinon la langue du compte). Prête pour un rapport client ; l’outil MCP get_grid la joint à sa réponse.

**GET /v1/grids/{id}/image**

```bash
curl "https://api.geogridapi.io/v1/grids/8f3c2a6e-…/image?lang=fr" \
  -H "Authorization: Bearer gga_live_…" \
  -o carte.png
```

Gratuite. 409 grid_not_ready tant que la grille tourne, grid_failed si elle a échoué. 30 images par minute et 500 par jour par compte.

## Évolution d’une fiche

GET /v1/history renvoie la série des grilles terminées d’une même carte (même fiche, mot-clé, format, rayon et centre), de la plus ancienne à la plus récente, et l’écart entre la première et la dernière (change.average_rank négatif = mieux). Désignez la carte par place_id et keyword (la grille la plus récente fixe le format) ou par grid_id. limit de 1 à 100, 50 par défaut ; has_more indique des grilles plus anciennes. Gratuit.

**GET /v1/history**

```bash
curl "https://api.geogridapi.io/v1/history?place_id=ChIJ…&keyword=plombier" \
  -H "Authorization: Bearer gga_live_…"
```

**Réponse**

```json
{
  "object": "history",
  "reference_grid_id": "8f3c…",
  "keyword": "plombier", "shape": "square", "size": 7, "radius_km": 3,
  "data": [
    { "grid_id": "41d0…", "created_at": "2026-09-01T05:00:12Z", "average_rank": 7.2, "top3_points": 10, "found_points": 40, "points_total": 49 },
    { "grid_id": "8f3c…", "created_at": "2026-10-13T05:00:09Z", "average_rank": 4.3, "top3_points": 28, "found_points": 48, "points_total": 49 }
  ],
  "has_more": false,
  "change": { "since": "2026-09-01T05:00:12Z", "average_rank": -2.9, "top3_points": 18, "found_points": 8 }
}
```

## Plusieurs grilles en un appel

POST /v1/batches lance jusqu’à 50 grilles au même format : une fiche sur plusieurs mots-clés, ou plusieurs fiches sur un mot-clé. Le format se donne une fois pour le lot (shape, size ou density, radius_km, language, country, webhook_url) ; chaque élément de items porte place_id ou query, center en option, et keyword. L’en-tête Idempotency-Key est accepté.

**POST /v1/batches**

```bash
curl https://api.geogridapi.io/v1/batches \
  -H "Authorization: Bearer gga_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: audit-lille-2026-10" \
  -d '{
    "size": 7,
    "items": [
      { "place_id": "ChIJ…", "keyword": "plombier" },
      { "place_id": "ChIJ…", "keyword": "plombier urgence" },
      { "query": "Plomberie Wazemmes, Lille", "keyword": "plombier" }
    ]
  }'
```

Le lot est refusé d’emblée (402) si le solde ne couvre pas credits_estimated (points de la forme × grilles ; les points en mer ne seront pas débités). Les grilles partent au fil des places libres (10 grilles en cours par compte), chacune comme une grille ordinaire : débit au lancement, remboursement si elle échoue, webhook à la fin.

GET /v1/batches/{id} suit le lot : status running puis completed, counts, et pour chaque grille son statut (queued, pending, running, completed, failed), son grid_id, son summary ou son error. 3 lots en lancement par compte au plus ; une grille qui n’a pas trouvé de place en 2 h est close sans débit (expired), et révoquer la clé qui a lancé le lot arrête ses grilles en attente (key_revoked).

**GET /v1/batches/{id}**

```json
{
  "id": "b7a1…", "object": "batch", "status": "running",
  "items_count": 3, "credits_estimated": 147, "credits_charged": 49,
  "counts": { "queued": 1, "pending": 0, "running": 1, "completed": 1, "failed": 0 },
  "items": [
    { "index": 0, "keyword": "plombier", "status": "completed", "grid_id": "8f3c…",
      "place": { "place_id": "ChIJ…", "name": "Plomberie Vauban" },
      "summary": { "average_rank": 4.3, "top3_points": 28, "found_points": 48 }, "error": null },
    { "index": 1, "keyword": "plombier urgence", "status": "running", "grid_id": "c21e…", … },
    { "index": 2, "keyword": "plombier", "status": "queued", "grid_id": null, … }
  ]
}
```

## Cartes répétées

POST /v1/schedules relance une grille existante chaque semaine (weekday : 1 = lundi … 7 = dimanche) ou chaque mois (month_day : 1 à 28), vers 7 h, heure de Paris. Même fiche, mot-clé, format et rayon ; chaque passage débite ses crédits et appelle votre webhook.

**POST /v1/schedules**

```bash
curl https://api.geogridapi.io/v1/schedules \
  -H "Authorization: Bearer gga_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "grid_id": "8f3c2a6e-…", "frequency": "weekly", "weekday": 1 }'
```

GET /v1/schedules liste vos cartes répétées ; PATCH /v1/schedules/{id} avec { "status": "paused" | "active" } les met en pause ou les relance ; DELETE /v1/schedules/{id} les arrête. Solde insuffisant : la carte répétée s’arrête (status stopped) et vous recevez un e-mail ; relancez-la après avoir rechargé.

## Trouver une fiche et lire le solde

**GET /v1/places/search**

```bash
curl "https://api.geogridapi.io/v1/places/search?q=Plomberie%20Vauban%20Lille&language=fr" \
  -H "Authorization: Bearer gga_live_…"
```

GET /v1/balance renvoie votre solde de crédits. Ces deux appels sont gratuits.

## Webhooks

À la fin de chaque grille, GeoGridAPI envoie un POST JSON : type grid.completed ou grid.failed, data = l’objet grille. Trois essais : immédiat, après 1 minute, après 10 minutes. Répondez par un code 2xx.

Chaque appel est signé : en-tête GeoGridAPI-Signature: t=\<horodatage>,v1=\<signature>, où la signature est un HMAC-SHA256 de « t.corps » avec votre secret whsec_ (console, section Webhook). Refusez un horodatage trop ancien.

**Vérification (Node.js)**

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the raw request body, exactly as received.
export function verifyGeoGridSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

**Vérification (Python)**

```python
import hashlib, hmac, time

def verify_geogrid_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    t = int(parts.get("t", "0"))
    if not t or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## Erreurs et limites

Toutes les erreurs ont la forme { "error": { "code", "message" } }.

| Statut | Code | Signification |
| --- | --- | --- |
| `400` | `invalid_body / invalid_json` | Corps ou paramètres invalides ; le message nomme le champ. |
| `401` | `invalid_key` | Clé absente, inconnue ou révoquée. |
| `402` | `insufficient_points` | Crédits insuffisants ; required et balance sont indiqués. |
| `404` | `not_found / place_not_found` | Grille inconnue pour ce compte, ou aucune fiche correspondante. |
| `409` | `grid_not_ready / grid_failed / already_scheduled` | Image d’une grille en cours ou échouée ; grille déjà répétée (schedule_id indiqué). |
| `422` | `schedule_limit` | 100 cartes répétées par compte au plus. |
| `429` | `rate_limited / too_many_open_grids / too_many_open_batches` | Plus de 60 requêtes par minute et par clé, 10 grilles en cours ou 3 lots en lancement. |
| `503` | `engine_unavailable` | Moteur momentanément indisponible ; réessayez (en-tête Retry-After). |

Une grille qui échoue est remboursée automatiquement. Prix : 1 crédit par point de la grille, packs de crédits dès 20 € HT, crédits sans expiration, 500 crédits offerts dès l’inscription.

## Serveur MCP

Le serveur MCP donne la grille à Claude, ChatGPT, Cursor et à tout client MCP compatible Streamable HTTP. Outils : find_business, estimate_grid (prix exact), run_grid (mêmes formats que l’API : shape, size ou density, radius_km), run_grids et get_batch (jusqu’à 50 grilles en un appel), get_grid, list_grids (grilles passées), get_history (évolution), schedule_grid, list_schedules, update_schedule et delete_schedule (grilles répétées), get_balance. Une grille terminée revient avec l’image de la carte, qui est devant et l’écart avec la carte précédente. Il utilise vos clés : une clé de test pour essayer gratuitement.

Six raccourcis prêts à l’emploi : dans Claude, menu + puis GeoGridAPI ; dans Claude Code, commandes /mcp__geogridapi__… Local visibility audit (local_audit), Who ranks ahead (who_ranks_ahead), Several keywords (several_keywords, jusqu’à 50 mots-clés), Ranking trend (ranking_trend), Track my listing (track_listing, chaque semaine ou chaque mois) et Client report (client_report). Chacun demande la fiche et le mot-clé, annonce le prix avant de lancer une grille, puis répond dans la langue du mot-clé.

Claude (claude.ai, application) et ChatGPT : ajoutez un connecteur personnalisé avec l’adresse https://api.geogridapi.io/mcp. La connexion ouvre GeoGridAPI : connectez-vous, puis autorisez l’assistant. Les grilles lancées ainsi sont réelles et débitent vos crédits.

Claude Code : lancez la commande, puis /mcp, geogridapi et Authenticate : la connexion se fait dans le navigateur, sans clé à copier. Sur une machine sans navigateur (CI, serveur), passez une clé dans l’en-tête.

**Claude Code**

```bash
claude mcp add --transport http geogridapi https://api.geogridapi.io/mcp
```

**Claude Code, avec une clé**

```bash
claude mcp add --transport http geogridapi https://api.geogridapi.io/mcp \
  --header "Authorization: Bearer gga_live_…"
```

**Cursor (.cursor/mcp.json)**

```json
{
  "mcpServers": {
    "geogridapi": {
      "url": "https://api.geogridapi.io/mcp",
      "headers": { "Authorization": "Bearer gga_live_…" }
    }
  }
}
```

Le serveur figure au registre MCP officiel sous le nom io.geogridapi/geogridapi.

- [Page du serveur MCP (en anglais)](https://geogridapi.io/en/mcp): Les 13 outils et leur coût, l’installation client par client, les clés de test et des exemples de prompts.

## SDK et outils

Clients officiels et outils, en open source sur GitHub (github.com/geogridapi). Ils suivent l’API v1 et utilisent les mêmes clés.

**Installation**

```bash
npm install geogridapi
pip install "git+https://github.com/geogridapi/geogridapi-python"
```

- [SDK Node.js et TypeScript (npm geogridapi)](https://www.npmjs.com/package/geogridapi): Client typé sans dépendance, ESM et CommonJS. Il attend la fin d’une grille et traverse les erreurs 429 et 5xx.
- [SDK Python (geogridapi-python)](https://github.com/geogridapi/geogridapi-python): Client synchrone et asynchrone (httpx). En attendant sa publication sur PyPI, installez-le depuis GitHub.
- [Outils LangChain (langchain-geogridapi)](https://github.com/geogridapi/langchain-geogridapi): Les 13 outils du serveur MCP en outils LangChain pour vos agents, avec une version en lecture seule. Publication sur PyPI à venir.
- [Serveur MCP en stdio (geogridapi-mcp)](https://github.com/geogridapi/geogridapi-mcp): Pour les clients MCP sans Streamable HTTP : npx -y github:geogridapi/geogridapi-mcp, avec GEOGRIDAPI_API_KEY.
- [local-rank-heatmap](https://github.com/geogridapi/local-rank-heatmap): Une commande transforme une fiche et un mot-clé en carte HTML, PNG, CSV et JSON.
- [Nœud n8n (n8n-nodes-geogridapi)](https://www.npmjs.com/package/n8n-nodes-geogridapi): Les cartes dans vos workflows n8n, sans code.
- [Registre MCP officiel](https://registry.modelcontextprotocol.io/v0/servers?search=io.geogridapi%2Fgeogridapi): Fiche io.geogridapi/geogridapi, reprise par les annuaires de serveurs MCP.
