> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gabin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Vector tiles

> Draw Gabin map layers (buildings, parcels, owners, energy, transactions, planning…) in MapLibre GL or Mapbox GL with Mapbox Vector Tiles.

`GET /v1/tiles/{layer}/{z}/{x}/{y}.pbf` serves the layers of the gabin.ai map as Mapbox Vector Tiles (MVT, XYZ scheme), for MapLibre GL JS, Mapbox GL JS or any MVT client. Use tiles when you want to draw many buildings or parcels at once; use `GET /v1/buildings` when you need the data of a few hundred buildings (see [buildings](https://docs.gabin.ai/guides/buildings.md#find-buildings-in-an-area)).

```bash theme={null}
curl "https://api.gabin.ai/v1/tiles/buildings/16/33648/23376.pbf" \
  -H "Authorization: Bearer $GABIN_API_KEY" \
  -o buildings.pbf
```

The response is `application/vnd.mapbox-vector-tile` with `Cache-Control: private, max-age=3600`.

## Layers

Each layer is served on a zoom range. A request outside it returns `400 INVALID`:

```json theme={null}
{
  "error": {
    "code": "INVALID",
    "message": "Layer buildings is served from zoom 15 to 16, with x and y below 2^z.",
    "docs_url": "https://docs.gabin.ai/errors#invalid"
  }
}
```

The **source layer** is the name of the layer inside the tile. You need it as `source-layer` in MapLibre and Mapbox styles.

| `layer` | Zoom | Source layer(s) | Content |
| - | - | - | - |
| `buildings` | 15–16 | `batiments` | BDNB building footprints |
| `parcels` | 15–16 | `parcelles` | Cadastral parcels |
| `districts` | 10–13 | `quartiers` | Districts (merged IRIS) |
| `owners` | 15–16 | `owners` | Corporate owners, as points with their building id |
| `occupants` | 15–16 | `occupants` | Establishments of legal entities, grouped by cell |
| `condominiums` | 15–16 | `copro` | Registered condominiums |
| `fiber` | 15–16 | `fiber-eligibility` | Fiber status |
| `transactions` | 15–16 | `dvf` | Property sales (DVF) |
| `energy` | 15–16 | `dpe` | Energy certificates (DPE) |
| `market` | 0–22 | `marche` | Market and territory figures by zone |
| `planning` | 13–22 | `urbanisme`, or `friches` with `view=friches` | Local plan zones, brownfields |
| `permits` | 15–16 | `sitadel` | Building permits |
| `heritage` | 13–22 | `abf-batiments` (zoom 15 and above), `abf-perimetres` (with `perimetres=true`) | Buildings under a heritage easement, protection perimeters |
| `ev_charging` | 12–15 | `irve-stations` | EV charging stations |
| `parking` | 13–15 | `parkings` | Parkings |

Set `minzoom` and `maxzoom` of your map source to the layer's range. The map then requests no tile below the range, and above it reuses the tiles of the last level (overzoom), so it never asks the API for a zoom it refuses.

### Feature properties

Feature properties are those of the gabin.ai map. Ids in them open the matching endpoints: `batiment_groupe_id` goes to `GET /v1/buildings/{id}`, `parcelle_id` to `GET /v1/parcels/{id}`.

> [accordion] buildings (source layer batiments)
>
> `batiment_groupe_id`, `code_commune_insee`, `surface_m2`, `nb_log` (dwellings), `nb_log_bucket`, `nb_niveau` (levels), `nb_niveau_bucket`, `annee_construction`, `mat_mur_bucket`, `mat_toit_bucket`, `max_hauteur`, `acceslibre_erp_count`, `acceslibre_a_ascenseur`, `statut_fibre_bucket`, `is_friche`, `dpe` (energy class `A` to `G`), `entreprises` (linked legal entities).
>
> ```json theme={null}
> {
>   "batiment_groupe_id": "bdnb-bg-SVG6-78J8-2VEU",
>   "code_commune_insee": "69382",
>   "surface_m2": "255",
>   "nb_log": 8,
>   "nb_niveau": 6,
>   "annee_construction": 1880,
>   "max_hauteur": "25",
>   "dpe": "E",
>   "entreprises": 13
> }
> ```

> [accordion] parcels (source layer parcelles)
>
> `parcelle_id`, `code_commune_insee`, `surface_m2`, `code_section`, `numero_plan`.
>
> ```json theme={null}
> {
>   "parcelle_id": "69382000AC0035",
>   "code_commune_insee": "69382",
>   "surface_m2": "132",
>   "code_section": "AC",
>   "numero_plan": "0035"
> }
> ```

> [accordion] owners (source layer owners)
>
> `label`, `owner_kind`, `fj_bucket`, `nb_locaux`, `cat_ent`, `naf_div`, `effectif_bucket`, `is_gest`, `batiment_groupe_id`, `nb_proprietaires`, `ca_bucket`, `sig_perte`, `sig_cp_neg`.

> [accordion] market (source layer marche)
>
> `code` (zone code), `nom` (zone name), `maille` (zone level, such as `quartier` or `commune`), `value` (the metric value).

> [accordion] heritage (source layers abf-batiments, abf-perimetres)
>
> `abf-batiments`: `batiment_groupe_id`, `kind` (`monument`, `abords`, `spr`, `site`). `abf-perimetres`: `idass`, `kind`, `nomass`.

Property names are in French and match the gabin.ai map. Inspect a tile to see the properties of the other layers.

### Layer options

Some layers accept filters in the query string. They are passed as they are; unknown keys are ignored.

| `layer` | Options |
| - | - |
| `buildings` | `acceslibre`, `friche`, `parking` (`true`/`false`: extra properties) |
| `owners`, `occupants`, `condominiums` | `iris`, `commune` (INSEE code) or `cp` (postal code) |
| `transactions` | `iris`/`commune`/`cp`, `prix_min`, `prix_max`, `date_min`, `date_max` (`YYYY-MM-DD`), `type_local`, `vente_multilot`, `surface_min`, `surface_max`, `pieces_min`, `pieces_max` |
| `energy` | `iris`/`commune`/`cp`, `etiquettes` (`A,B`…), `date_min`, `date_max`, `jeux`, `type_batiment`, `surface_min`, `surface_max`, `generateur`, `isolation` |
| `market` | `metric`: `prix_residentiel`, `prix_appartement`, `prix_maison`, `prix_evolution`, `prix_appartement_evolution`, `prix_maison_evolution`, `loyer_appart`, `loyer_t1t2`, `loyer_t3plus`, `loyer_maison`, `loyer_appart_evolution`, `loyer_t1t2_evolution`, `loyer_t3plus_evolution`, `loyer_maison_evolution`, `pop_total`, `pop_evolution`, `pct_cadres`, `revenu_median_disp`, `revenu_evolution`, `taux_pauvrete`, `indice_gini` |
| `planning` | `view`: `zones` (default) or `friches` |
| `permits` | `type`, `statut`, `annee_min`, `annee_max` |
| `heritage` | `kinds` (`monument`, `abords`, `spr`, `site`), `perimetres` (`true`/`false`), `iris`/`commune`/`cp` |

For example, apartment prices by zone:

```bash theme={null}
curl "https://api.gabin.ai/v1/tiles/market/12/2103/1461.pbf?metric=prix_appartement" \
  -H "Authorization: Bearer $GABIN_API_KEY" \
  -o market.pbf
```

## Empty tiles

A tile with no feature returns `204 No Content` with an empty body. If you fetch tiles yourself, treat a 204 as an empty tile, not as an error.

## Weight and limits

| Tile | Weight |
| - | - |
| Served from the server cache (10 minutes) | 0.1 |
| Read from the database | 0.5 |

The cache is shared by all accounts and keyed by layer, coordinates and options, so a tile someone requested in the last 10 minutes costs 0.1.

A map view requests many tiles at once. Two limits matter:

* **Rate and quota.** Tiles count against your per-minute rate and daily quota like any request, at their weight. See [limits](https://docs.gabin.ai/limits.md).
* **Concurrent requests.** A tile read from the database counts toward the limit of 2 concurrent requests per account. Above it, the API answers `429 RATE_LIMITED` with `Retry-After: 1` and gives the weight back. Cached tiles are not subject to this limit.

When a map loads a new area, several uncached tiles can be refused this way and stay blank. A server-side proxy that sends at most 2 tile requests at a time and retries on 429 avoids it. See [keep your key on the server](#keep-your-key-on-the-server).

## Add a Gabin layer to MapLibre GL JS

MapLibre's `transformRequest` adds the `Authorization` header to the tile requests sent to `api.gabin.ai`. This page draws the building footprints, colored by energy class, and logs the building id on click:

```html theme={null}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
    <script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
    <style>
      html, body, #map { margin: 0; height: 100%; }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      // Visible to anyone who opens this page: for local prototypes only.
      const GABIN_API_KEY = "gab_live_…";

      const map = new maplibregl.Map({
        container: "map",
        center: [4.8357, 45.7637],
        zoom: 16,
        minZoom: 15,
        style: {
          version: 8,
          sources: {},
          layers: [{ id: "background", type: "background", paint: { "background-color": "#f4f1ea" } }],
        },
        transformRequest: (url) => {
          if (url.startsWith("https://api.gabin.ai/")) {
            return { url, headers: { Authorization: `Bearer ${GABIN_API_KEY}` } };
          }
        },
      });

      map.on("load", () => {
        map.addSource("gabin-buildings", {
          type: "vector",
          tiles: ["https://api.gabin.ai/v1/tiles/buildings/{z}/{x}/{y}.pbf"],
          minzoom: 15,
          maxzoom: 16,
        });

        map.addLayer({
          id: "buildings",
          type: "fill",
          source: "gabin-buildings",
          "source-layer": "batiments",
          paint: {
            "fill-color": [
              "match",
              ["get", "dpe"],
              "A", "#1a9850",
              "B", "#66bd63",
              "C", "#a6d96a",
              "D", "#fee08b",
              "E", "#fdae61",
              "F", "#f46d43",
              "G", "#d73027",
              "#cccccc",
            ],
            "fill-outline-color": "#555555",
          },
        });

        map.on("click", "buildings", (e) => {
          const id = e.features[0].properties.batiment_groupe_id;
          console.log(id); // then GET /v1/buildings/{id} from your server
        });
      });
    </script>
  </body>
</html>
```

The background-only style keeps the example free of third-party keys. In a real map, start from your basemap style and call `addSource` and `addLayer` the same way. With Mapbox GL JS, `transformRequest` works the same.

> [!WARNING]
>
> An API key in browser code is visible to anyone who opens the page, in the source and in the network panel. Whoever copies it can spend your quota, and every call is attributed to your account. Keep keys on your server: proxy the tiles, or serve the page only on a local machine while you prototype. If a key leaks, revoke it at [gabin.ai/compte/api](https://gabin.ai/compte/api).

## Keep your key on the server

A small proxy keeps the key server-side, sends at most 2 tile requests at a time to Gabin, and retries on 429. The browser then requests your own URL, without `transformRequest`.

```javascript proxy.mjs theme={null}
// Node.js 18 or later: node proxy.mjs
import http from "node:http";

const KEY = process.env.GABIN_API_KEY;
const TILE_PATH = /^\/tiles\/[a-z_]+\/\d+\/\d+\/\d+\.pbf$/;

// At most 2 requests to Gabin at a time; the others wait their turn.
let active = 0;
const waiting = [];
const acquire = () =>
  active < 2 ? (active++, Promise.resolve()) : new Promise((resolve) => waiting.push(resolve));
const release = () => {
  const next = waiting.shift();
  if (next) next();
  else active--;
};

async function fetchTile(pathAndQuery) {
  for (let attempt = 0; attempt < 3; attempt++) {
    await acquire();
    let res;
    let body;
    try {
      res = await fetch(`https://api.gabin.ai/v1${pathAndQuery}`, {
        headers: { Authorization: `Bearer ${KEY}` },
      });
      body = Buffer.from(await res.arrayBuffer());
    } finally {
      release();
    }
    if (res.status !== 429) return { res, body };
    const seconds = Number(res.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
  return { res: { status: 503, headers: new Headers() }, body: Buffer.alloc(0) };
}

http
  .createServer(async (req, res) => {
    const url = new URL(req.url, "http://localhost");
    if (!TILE_PATH.test(url.pathname)) {
      res.writeHead(404).end();
      return;
    }
    const tile = await fetchTile(url.pathname + url.search);
    res.writeHead(tile.res.status, {
      "Content-Type": tile.res.headers.get("content-type") ?? "application/vnd.mapbox-vector-tile",
      "Cache-Control": "private, max-age=3600",
      "Access-Control-Allow-Origin": "*",
    });
    res.end(tile.body);
  })
  .listen(8080);
```

In the map, point the source at the proxy and drop `transformRequest`:

```javascript theme={null}
map.addSource("gabin-buildings", {
  type: "vector",
  tiles: ["http://localhost:8080/tiles/buildings/{z}/{x}/{y}.pbf"],
  minzoom: 15,
  maxzoom: 16,
});
```

Before you expose such a proxy publicly, add your own access control and rate limit: otherwise anyone can use your quota through it.

## Next steps

> [card] [Buildings](https://docs.gabin.ai/guides/buildings.md)
>
> Open the building a user clicked: profile, companies, owners, sections.

> [card] [Limits](https://docs.gabin.ai/limits.md)
>
> Rate, quota, concurrency and the weight of each request.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.