# Taxon Partner API 2.0 : guide d'intégration (FR)

Version 2.5.1, 16-09-2026. Ce guide est la traduction française du guide anglais integration-guide.md (EN) ; version néerlandaise : gids-nl.md (NL). Contrat anglais ; voir la section 14 pour la correspondance depuis les noms néerlandais 1.x (la même table est la section 12 de la documentation HTML). Documentation complète : https://taxonapi.be/partner-docs/ (EN), /partner-docs/index-fr (FR), /partner-docs/index-nl (NL) ; lisible par machine : /partner-docs/openapi.yaml.
Destiné au personnel technique du partenaire. Aucun prix dans ce document ; ils figurent dans la convention. La convention, les prix et vos clés sont confidentiels ; cette documentation elle-même est publiée ouvertement.

## 1. Démarrage rapide en cinq lignes

1. URL de base `https://taxonapi.be/api/v1/partner/`, HTTPS uniquement, de serveur à serveur.
2. Chaque requête : `X-Api-Key` (tx_live_ ou tx_test_) et `X-User-Ref` (obligatoire, votre identifiant d'utilisateur). Recommandé : `Idempotency-Key` (UUID par requête), `Accept-Language: en` (ou `fr`, `nl`). `X-Case-Ref` (votre numéro de dossier) est techniquement optionnel (une requête sans lui est acceptée ; `billing.case_ref` vaut alors `null` et la colonne CSV est vide) mais contractuellement obligatoire : chaque requête appartient à un dossier d'expertise, envoyez-le donc toujours.
3. `GET /address?address=...&type=house|apartment|land&sections=basic[,avm][&living_area_m2=&year_built=&epc=&condition=&bedrooms=&plot_area_m2=&capakeys=]`. `sections=basic` (par défaut) est le pack de base : `basic` + `parcel` + `price_map` ensemble, un prix par dossier ; `avm` est une option distincte (demander `parcel` ou `price_map` séparément est normalisé vers le pack). `living_area_m2` est obligatoire pour `avm` (pas pour `type=land` : pas d'AVM pour un terrain, voir la sous-section Terrains de la section 2). Lorsqu'un bien se compose de plusieurs parcelles (jardin, garage, prairie), transmettez-les en `capakeys` (au plus 10) : d'abord une requête sans, pour obtenir les candidates, puis une avec (section 3).
4. Même utilisateur + même bien dans les 30 jours = gratuit (`from_cache: true` ou messages `dedup` par section) ; l'option AVM ajoutée plus tard = seulement cette option ; un autre utilisateur = nouvelle facturation. Même `Idempotency-Key` dans les 24 h = même réponse (`X-Idempotent-Replay: true`), non comptée. AVM sans `living_area_m2` n'est pas une erreur : HTTP 200 avec message `living_area_required`, section omise.
5. Affichez toujours `attribution` et `disclaimer` de la réponse, textuellement et visiblement ; conservez les réponses brutes au plus 30 jours ; photos : à récupérer pendant la validité du lien et à intégrer uniquement dans le rapport du dossier, avec une mention de source visible (section 5).

**Utilisateur.** Un utilisateur est l'unité que vous transmettez vous-même à chaque requête : une agence, un collaborateur, une succursale ou un gestionnaire de dossier. Ce choix détermine la facturation : le même utilisateur qui redemande le même bien dans les 30 jours ne paie pas une seconde fois (dédoublonnage) ; un autre utilisateur, oui. Un plafond journalier s'applique par utilisateur, un plafond global par clé. Plafonds : 20 requêtes par utilisateur et par jour, 300 par clé et par jour, 60 par minute avec une rafale de 20 (clé de test : 20 par jour). Ce qui compte pour les plafonds journaliers (et comme requête dans `/usage`) : chaque requête qui atteint la recherche d'adresse, donc 200 (fraîche, dédoublonnée ou partielle), 404 `address_not_found`, 422 `address_imprecise`, `capakey_too_far` et `type_unsupported`, 502 et 504 ; non comptés : 400, 401, 403, 405, 409, 422 `capakey_invalid`, 429, 503 et les rejeux par Idempotency-Key.

**Dossier.** `X-Case-Ref` est votre numéro de dossier (`A-Z a-z 0-9 . _ : @ / space -`, au plus 64). Il revient dans `billing.case_ref` et dans le CSV de consommation, pour que chaque requête puisse être rattachée à un dossier dans votre comptabilité. Chaque requête appartient à un dossier d'expertise concret (règle contractuelle).

## 2. curl

```bash
# Health (no key)
curl -s https://taxonapi.be/api/v1/partner/health

# basic package + AVM option, Walloon address, English labels
curl -s -G "https://taxonapi.be/api/v1/partner/address" \
  -H "X-Api-Key: $TAXON_API_KEY" \
  -H "X-User-Ref: ag-0417" \
  -H "X-Case-Ref: PT-2026-004512" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Accept-Language: en" \
  --data-urlencode "address=Avenue Alphonse Allard 94, 1420 Braine-l'Alleud" \
  --data-urlencode "sections=basic,avm" \
  --data-urlencode "type=house" \
  --data-urlencode "living_area_m2=165" \
  --data-urlencode "year_built=1978" \
  --data-urlencode "epc=D" \
  --data-urlencode "condition=good" \
  -D - | sed -n '1,20p;/^{/,$p'

# Usage of a month as CSV (Excel-ready, ; and decimal comma)
curl -s "https://taxonapi.be/api/v1/partner/usage?month=2026-09&format=csv" \
  -H "X-Api-Key: $TAXON_API_KEY" -o taxon-usage-2026-09.csv
```

Ne mettez jamais la clé sur la ligne de commande dans des environnements partagés ; utilisez une variable d'environnement ou un coffre à secrets.

Ce que renvoie le premier appel (réel, 07-09-2026, clé de test, abrégé) : `sections_delivered: ["basic", "parcel", "avm", "price_map"]`, `address.region: "WAL"`, `address.nis_code: "25014"`, `parcel.capakey: "25744E0226/00_000"`, `parcel.zoning.plan: "plan de secteur"`, `avm.value` avec `range`, `range_90`, `rental_value_range` et `confidence.label: "medium"`, `avm.price_basis: "asking_price_model"`, message `avm_indicative_not_regionally_calibrated` (Wallonie), `attribution: "References: Taxon (taxon.be)"`. Les exemples utilisent `uuidgen` et `jq` (Linux/macOS) ; sous Windows PowerShell, utilisez `[guid]::NewGuid()` pour la clé et omettez le filtre `| jq` (avec une valeur vide, curl n'envoie aucun en-tête `Idempotency-Key`).

**Consommation par clé API (2.4.1).** `GET /usage` porte aussi le bloc `per_key` (entre `per_user` et `per_day`) : la consommation par clé API, avec les mêmes filtres que le reste de la réponse (mois, l'environnement de la clé appelante, `user_ref`). Par clé : `prefix` (les 12 premiers caractères, comme dans votre liste de clés), `label` (`null` si vide), `environment`, `active`, `revoked_at` (`null` tant que la clé est active), `requests`, `cases`, `charged`, `sections`, `sections_delivered`, `amount` et `last_activity` ; triées par `amount`, puis `requests`, par ordre décroissant. Les clés sans requête dans le mois ne sont pas listées ; une clé révoquée avec des requêtes dans ce mois l'est (`active: false`, `revoked_at` renseigné). Les sommes sur `per_key` sont égales à `total`. Le CSV (`format=csv`) reçoit `key_prefix` comme 16e et dernière colonne. Exemple avec une clé live actuelle et une clé live précédente révoquée depuis :

```json
"per_key": [
  {
    "prefix": "tx_live_3f9a",
    "label": "production",
    "environment": "live",
    "active": true,
    "revoked_at": null,
    "requests": 3,
    "cases": 3,
    "charged": 3,
    "sections": {"package": 2, "avm": 2},
    "sections_delivered": {"basic": 3, "parcel": 3, "avm": 2, "price_map": 3},
    "amount": 10.0,
    "last_activity": "2026-09-08T10:39:27+02:00"
  },
  {
    "prefix": "tx_live_b71c",
    "label": "old key",
    "environment": "live",
    "active": false,
    "revoked_at": "2026-09-08T10:39:20+02:00",
    "requests": 1,
    "cases": 1,
    "charged": 1,
    "sections": {"package": 1, "avm": 0},
    "sections_delivered": {"basic": 1, "parcel": 1, "avm": 0, "price_map": 1},
    "amount": 3.0,
    "last_activity": "2026-09-08T10:39:15+02:00"
  }
]
```

### Terrains (`type=land`, 2.5.0)

`type=land` (alias `grond`, `terrain`, `bouwgrond`) demande le paquet pour un terrain à bâtir. Par rapport à une maison ou un appartement : un seul bloc de points de référence (`type: land`, `transaction: sale`, pas de bloc location) avec des annonces de terrains à vendre (terrains à bâtir, terrains à projet et terrains sans sous-type plus précis ; les terres agricoles, prairies, bois, vergers, terrains industriels et PME, emplacements de parking, garages et terrains récréatifs sont exclus sur la base du sous-type annoncé) de toutes les années de publication (2.5.1 : plus de limite d'âge, pour que vous puissiez actualiser les prix demandés des annonces plus anciennes selon l'évolution du marché dans le cadre du dossier (indexation des prix) ; les conditions de licence restent inchangées : pas de fusion, d'indexation ni de conservation entre dossiers), dans un rayon de 5 km autour de l'adresse (étendu une fois à 10 km s'il y en a moins de 10 au total ; toujours moins de 10 = champ de bloc `low_sample: true` et message `low_sample`), classées avec d'abord les annonces des 24 derniers mois (triées par distance) puis les plus anciennes (triées par distance), 25 au plus par bloc ; chaque élément porte `age_months` (mois civils entiers entre `published` et aujourd'hui, 0 pour le mois en cours) et le champ de bloc `max_age_months` vaut `null` pour un terrain ; chaque élément porte `plot_area_m2` et `price_per_m2_plot` (prix demandé par m² de terrain), les champs de logement (`living_area_m2`, `price_per_m2`, `bedrooms`, `epc_label`, `year_built`, `condition`, `building_type`, `new_build`) valent `null`, `features` se limite à `{plot_area_m2, zoning, flood_zone, land_type}` (`zoning` vaut actuellement toujours `null` ; l'affectation de la parcelle du bien se trouve dans `parcel.zoning` ; `land_type` vaut `building_plot`, `project_land` ou `other` et détermine le premier mot de `summary` : « Terrain à bâtir », « Terrain à projet » ou « Terrain ») et `summary` donne par exemple « Building plot of 694 m² in Kluisbergen, listed since 10-09-2026. ». `neighbourhood.land_price_level {radius_m, count, price_per_m2_plot {p25, median, p75}, price_kind: asking_price, max_age_months: 24 | 60}` provient des mêmes annonces de terrains (mêmes exclusions) des 24 derniers mois (un niveau de prix doit être actuel ; 2.5.1 : avec moins de 10 annonces utilisables dans les 24 mois, la fenêtre est élargie à 60 mois et `max_age_months` indique la fenêtre utilisée, 24 ou 60) ; seules les annonces avec une superficie et un prix demandé entre 15 et 5 000 EUR par m² de terrain entrent dans les quartiles (`count`), afin que les terres agricoles annoncées comme constructibles et les superficies fictives ne faussent pas le niveau (les éléments hors de cette bande restent dans la liste avec leur propre `price_per_m2_plot`) ; `price_level` et `epc_prices` valent `null` (message `housing_stats_not_available_for_land`). **Pas d'AVM pour un terrain** : `sections=basic,avm` reste HTTP 200 avec `avm: null`, le message `{code: avm_not_available_for_land, section: avm}` et aucune facturation de l'option ; `living_area_m2` peut être omis. `parcel` (l'essentiel pour un terrain : affectation, droit de préemption, plusieurs parcelles via `capakeys`) et `price_map` (la carte des logements, pas une carte des prix des terrains) sont inchangés ; la facturation est le pack de base comme d'habitude.

```bash
curl -s -G "https://taxonapi.be/api/v1/partner/address" \
  -H "X-Api-Key: $TAXON_API_KEY" \
  -H "X-User-Ref: office-oudenaarde-02" \
  -H "X-Case-Ref: DOS-2026-0518" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Accept-Language: en" \
  --data-urlencode "address=Buissestraat 17, 9690 Kluisbergen" \
  --data-urlencode "type=land" \
  --data-urlencode "sections=basic,avm"
```

Réponse (réelle, 15-09-2026, clé de test, requête `f0c372798b3f285b66ab4b584819be57` ; champs 2.5.1 `age_months` et `max_age_months: null` ajoutés), raccourcie à un point de référence avec une photo et aux champs principaux de la parcelle ; `building_stock`, `safety`, `amenities`, `parcels`, `parcel_candidates` et `price_map` remplacés par `"..."` :

```json
{
  "request_id": "f0c372798b3f285b66ab4b584819be57",
  "environment": "test",
  "address": {
    "input": "Buissestraat 17, 9690 Kluisbergen",
    "normalized": "Buissestraat 17, 9690 Kluisbergen",
    "box": null,
    "postal_code": "9690",
    "municipality": "Kluisbergen",
    "lat": 50.76716,
    "lon": 3.484313,
    "region": "VL",
    "nis_code": "45060",
    "geocoder": "geo.api.vlaanderen.be",
    "geocode_score": 0.95,
    "precision": "house_number"
  },
  "sections_delivered": [
    "basic",
    "parcel",
    "price_map"
  ],
  "basic": {
    "comparables": [
      {
        "type": "land",
        "type_label": "land",
        "transaction": "sale",
        "transaction_label": "for sale",
        "address_mode": "house_number",
        "radius_m": 5000,
        "count": 25,
        "excluded_subject_property": 0,
        "max_age_months": null,
        "low_sample": false,
        "items": [
          {
            "ref": "r_9965f5493e",
            "address": "Buissestraat 19, 9690 Kluisbergen",
            "distance_m": 25,
            "type": "land",
            "transaction": "sale",
            "price": 80000,
            "price_kind": "asking_price",
            "price_kind_label": "asking price",
            "price_per_m2": null,
            "price_per_m2_plot": 115,
            "living_area_m2": null,
            "plot_area_m2": 694,
            "bedrooms": null,
            "epc_label": null,
            "epc_kwh_m2": null,
            "epc_source": null,
            "year_built": null,
            "condition": null,
            "building_type": null,
            "new_build": null,
            "published": "2026-09-10",
            "days_online": 4,
            "last_seen": "2026-09-14",
            "age_months": 0,
            "status": "online",
            "status_label": "online",
            "source": "listing",
            "source_label": "listing",
            "features": {
              "plot_area_m2": 694,
              "zoning": null,
              "flood_zone": null,
              "land_type": "building_plot"
            },
            "summary": "Building plot of 694 m² in Kluisbergen, listed since 10-09-2026.",
            "thumbnail": {
              "url": "https://taxonapi.be/api/v1/marketexplorer/foto/fp_WzI3NDc1OTEsIjAxYTA0NzM2LTE5OGUtN2Q3ZC1iZjNjLWYyZDQ2NzliOTI3ZSIsMTc4OTQ2MzYyNiwicCJd.QB9ILrzzIwQxlyOV?w=160",
              "valid_until": "2026-09-15T09:13:46Z"
            },
            "photo_count": 13,
            "history": [
              {
                "date": "2026-08-28",
                "price": 80000,
                "event": "published",
                "label": "published"
              }
            ],
            "photos": [
              {
                "url": "https://taxonapi.be/api/v1/marketexplorer/foto/fp_WzI3NDc1OTEsIjAxYTA0NzM2LTE5OGUtN2Q3ZC1iZjNjLWYyZDQ2NzliOTI3ZSIsMTc4OTQ2MzYyNiwicCJd.QB9ILrzzIwQxlyOV?w=640",
                "download_token": "b17f02c8feb98b013e60",
                "valid_until": "2026-09-15T09:13:46Z"
              }
            ]
          }
        ]
      }
    ],
    "neighbourhood": {
      "sector": {
        "code": "45060A010",
        "name": "BUISESTRAAT",
        "level": "sector",
        "municipality": "Kluisbergen"
      },
      "building_stock": "...",
      "price_level": null,
      "epc_prices": null,
      "safety": "...",
      "land_price_level": {
        "radius_m": 5000,
        "count": 116,
        "price_per_m2_plot": {
          "p25": 126,
          "median": 160,
          "p75": 224
        },
        "price_kind": "asking_price",
        "max_age_months": 24
      }
    },
    "amenities": "..."
  },
  "parcel": {
    "capakey": "45043C0460/00X000",
    "region": "VL",
    "area_m2": 528.79,
    "cadastral_area_m2": 528.78,
    "fiscal_situation": "2027-01-01",
    "zoning": {
      "category": "residential",
      "label": "woongebieden",
      "plan": "gewestplan"
    },
    "preemption_right": {
      "status": "none",
      "source": "RVV thematic layer, right of pre-emption (Flanders)"
    },
    "source": "CadGIS PlanParcellaire (FPS Finance) / GRB Gbg - building at ground level (Digitaal Vlaanderen)",
    "main_parcel": "45043C0460/00X000",
    "parcels_count": 1,
    "total_area_m2": 528.79,
    "plot_area_source": "cadastre_main_parcel",
    "parcels": "...",
    "parcel_candidates": "..."
  },
  "avm": null,
  "price_map": "...",
  "billing": {
    "user_ref": "office-oudenaarde-02",
    "case_ref": "DOS-2026-0518",
    "charged": false,
    "pilot": false,
    "free_reason": "test",
    "price": {
      "package": 0.0,
      "avm": 0.0,
      "total": 0.0,
      "currency": "EUR",
      "excl_vat": true
    },
    "dedup_of": null,
    "dedup_valid_until": "2026-10-15T10:13:47+02:00",
    "month": "2026-09",
    "month_to_date": {
      "cases": 0,
      "amount": 0.0
    },
    "pilot_status": {
      "active": true,
      "quota": 200,
      "used": 0,
      "remaining": 200,
      "ends": "2026-11-03",
      "expired": false
    },
    "today": {
      "client_today": 10,
      "user_today": 1
    }
  },
  "notices": [
    {
      "code": "avm_not_available_for_land",
      "section": "avm",
      "message": "AVM not delivered: Taxon does not provide an automated valuation for land (type land); not charged."
    },
    {
      "code": "housing_stats_not_available_for_land",
      "section": "basic",
      "message": "price_level and epc_prices are housing statistics and are not delivered for land; see neighbourhood.land_price_level."
    },
    {
      "code": "price_map_sparse",
      "section": "price_map",
      "message": "price map: fewer than 30 listings in the subject neighbourhood (small sample)."
    },
    {
      "code": "test_environment",
      "section": null,
      "message": "test key: not invoiced"
    }
  ],
  "upstream_errors": {},
  "attribution": "References: Taxon (taxon.be)",
  "disclaimer": "Asking prices from listings, not notarial sale prices. Data for internal use per case only; building a derived database is prohibited. The valuer decides, the AVM is a support tool.",
  "generated_at": "2026-09-15T10:13:47+02:00"
}
```

## 3. Plusieurs parcelles par bien : d'abord les candidates, puis les capakeys

**Source des données parcellaires (2.4.2).** La section parcel lit le plan parcellaire cadastral actuel du SPF Finances (CadGIS PlanParcellaire, situation fiscale courante, aujourd'hui 01-01-2027) ; l'extrait annuel INSPIRE (01-01-2026) n'est plus que la solution de repli en cas de panne ou de réponse vide. `fiscal_situation` (sur `parcel`, sur chaque élément de `parcels[]` et sur chaque élément de `parcel_candidates[]`) donne la date ISO de la situation fiscale dont provient la parcelle ; `source` mentionne la couche. La date est la situation fiscale depuis laquelle la version actuelle de cette parcelle s'applique : une parcelle inchangée depuis des années porte une date plus ancienne (p. ex. 2019-01-01), alors que le plan dont elle est lue est toujours la situation fiscale courante. Une date plus ancienne ne signifie donc pas des données obsolètes ; seul `2026-01-01` combiné à un `source` INSPIRE indique le repli sur l'extrait annuel. Une parcelle divisée ou fusionnée après le dernier extrait (p. ex. 45043C0460/00R000 à Kluisbergen : une parcelle de 921 m² dans l'extrait, 460R 392 m² + 460X 529 m² dans le plan actuel) est livrée sous sa forme actuelle.

Une maison mitoyenne avec un garage ou une parcelle de jardin séparés, une ferme avec des prairies, une villa sur deux parcelles cadastrales : la parcelle du point à l'adresse n'est alors qu'une partie du bien. L'API résout cela en deux étapes.

**Étape 1 : requête sans `capakeys`.** La section parcel contient la parcelle du point (`main_parcel`, `parcels[]` avec 1 élément) et `parcel_candidates[]` : les parcelles contiguës (au plus 12), chacune avec `capakey`, `area_m2` cadastrale, `direction` (point cardinal par rapport à la parcelle principale, toujours `N`, `E`, `S` ou `W` quelle que soit la langue, comme `orientation_garden`), `built` (true/false, ou `null` lorsque la couche régionale des bâtiments était temporairement indisponible) et `distance_m`. Affichez cette liste à l'utilisateur (par exemple « 33011I0172/00G000, 40 m², sud, bâtie » = le garage) et laissez-le cocher ce qui appartient au bien. Il n'y a pas de données de propriétaire ; l'utilisateur connaît le dossier.

```bash
curl -s -G "https://taxonapi.be/api/v1/partner/address" \
  -H "X-Api-Key: $TAXON_API_KEY" -H "X-User-Ref: ag-0417" -H "X-Case-Ref: PT-2026-004512" -H "Accept-Language: en" \
  --data-urlencode "address=Rijselstraat 62, 8900 Ieper" \
  --data-urlencode "sections=basic" --data-urlencode "type=house" \
  | jq '.parcel | {main_parcel, parcel_candidates}'
```

Réponse réelle du 07-09-2026 (`request_id` b78e1f9e939f860da0e244ab59e2ba49) :

```json
{
  "main_parcel": "33011I0172/00H000",
  "parcel_candidates": [
    {"capakey": "33011I0170/00C000", "area_m2": 319.23, "direction": "N", "built": true, "distance_m": 14},
    {"capakey": "33011I0172/00F000", "area_m2": 153.69, "direction": "W", "built": true, "distance_m": 23},
    {"capakey": "33011I0172/00G000", "area_m2": 40.1,   "direction": "S", "built": true, "distance_m": 28},
    {"capakey": "33011I0169/00K000", "area_m2": 2940.52, "direction": "W", "built": true, "distance_m": 49}
  ]
}
```

**Étape 2 : nouvelle requête avec `capakeys`** (la parcelle principale plus les parcelles cochées, séparées par des virgules, au plus 10 ; la forme avec tiret est acceptée). Même utilisateur, même adresse : le pack de base fourni à l'étape 1 n'est **pas refacturé** (message `dedup` avec `section: basic` ; `billing.free_reason` ne vaut `dedup` que lorsque rien de nouveau n'est fourni, sinon `null` (facturé), `test` ou `pilot`), même si la section parcel est reconstruite avec les nouvelles parcelles. Si vous ajoutez l'option AVM à l'étape 2, vous ne payez que cette option.

```bash
curl -s -G "https://taxonapi.be/api/v1/partner/address" \
  -H "X-Api-Key: $TAXON_API_KEY" -H "X-User-Ref: ag-0417" -H "X-Case-Ref: PT-2026-004512" -H "Accept-Language: en" \
  --data-urlencode "address=Rijselstraat 62, 8900 Ieper" \
  --data-urlencode "sections=basic,avm" --data-urlencode "type=house" \
  --data-urlencode "living_area_m2=140" --data-urlencode "bedrooms=3" \
  --data-urlencode "capakeys=33011I0172/00H000,33011I0172-00G000" \
  | jq '{parcel: (.parcel | {main_parcel, parcels_count, total_area_m2, zoning_combined, preemption_right_combined, parcels: [.parcels[] | {capakey, is_main, area_m2, distance_to_address_m}]}), avm_inputs: .avm.inputs_used, free_reason: .billing.free_reason}'
```

Réponse réelle du 07-09-2026 (`request_id` 2e88e0e1cfc66d6731aa1ca13f5ae83d, 6,8 s ; `free_reason` vaut ici `dedup` parce que cet utilisateur avait déjà reçu `avm` pour ce bien plus tôt ce jour-là) :

```json
{
  "parcel": {
    "main_parcel": "33011I0172/00H000",
    "parcels_count": 2,
    "total_area_m2": 669.02,
    "zoning_combined": {"category": "residential", "label": "woongebieden met cultureel, historische en/of esthetische waarde", "plan": "gewestplan"},
    "preemption_right_combined": {"status": "none", "parcels": []},
    "parcels": [
      {"capakey": "33011I0172/00H000", "is_main": true,  "area_m2": 628.92, "distance_to_address_m": 0},
      {"capakey": "33011I0172/00G000", "is_main": false, "area_m2": 40.1,   "distance_to_address_m": 26}
    ]
  },
  "avm_inputs": {"type": "house", "living_area_m2": 140.0, "year_built": null, "epc_label": null, "condition": null, "bedrooms": 3, "plot_area_m2": 669.02, "plot_area_source": "cadastre_2_parcels"},
  "free_reason": "dedup"
}
```

Règles et chemins d'erreur :

- Les champs au niveau de la section (`capakey`, `area_m2`, `zoning`, `preemption_right`, ...) restent ceux de la **parcelle principale** : la parcelle indiquée sur laquelle tombe le point de l'adresse, sinon la première parcelle indiquée (alors message `main_parcel_not_in_capakeys` avec la parcelle du point, pour que vous ne l'oubliiez pas). `zoning_combined` est un objet lorsque toutes les parcelles ont la même affectation, sinon une liste avec `parcels[]` par affectation. `preemption_right_combined.status` vaut `yes` dès qu'une parcelle se trouve dans un périmètre (avec les capakeys dans `parcels[]` et le détail par parcelle dans `details`).
- L'AVM (type house) utilise `total_area_m2` ; `avm.inputs_used.plot_area_source` indique `cadastre_main_parcel`, `cadastre_<n>_parcels` (n = nombre de parcelles fournies, 2 à 10, par exemple `cadastre_2_parcels`) ou `partner` ; le champ de section `parcel.plot_area_source` ne porte que les deux valeurs cadastre, `partner` n'apparaît que dans `avm.inputs_used`. Pour un appartement, aucune superficie de terrain n'est transmise au modèle (même avec `capakeys`).
- `plot_area_m2` est une valeur de repli : sans `capakeys` et avec votre propre total, l'AVM utilise celui-ci (message `plot_area_not_cadastral`) ; avec `capakeys`, le cadastre l'emporte toujours.
- `422 capakey_invalid` : format erroné ou plus de 10 clés (`details[]` nomme les parties erronées). `422 capakey_too_far` : une parcelle se trouve à plus de 2 km de l'adresse ; rien n'est fourni ni facturé (frein anti-abus : des parcelles éloignées de l'adresse n'appartiennent pas au bien). La section parcel fait partie du pack de base, `capakeys` ne requiert donc aucune section supplémentaire (`400 invalid_request` uniquement lorsque votre plan n'autorise pas la section parcel).
- Une capakey qui n'existe pas dans le plan parcellaire : message `capakey_not_found` (section parcel, le message nomme la parcelle) et `parcels_not_found[]` ; les autres parcelles sont fournies. Si l'analyse parcellaire échoue pour une parcelle, seule sa superficie cadastrale compte (message `parcel_analysis_incomplete`).
- Les mêmes `capakeys` à nouveau dans les 30 jours renvoient la réponse conservée dans le grand livre (`from_cache: true`) ; d'autres `capakeys` donnent une section parcel fraîche, également gratuite (même bien). La même `Idempotency-Key` avec d'autres `capakeys` = `409 idempotency_conflict`. `parcel_candidates`, `parcel_candidates_source` et `parcel_candidates_remark` sont toujours présents : `null` lorsque `capakeys` a été donné ; `parcel_candidates_remark` vaut aussi `null` lorsque la recherche des voisines a réussi.
- Charge : limitez-vous aux parcelles du dossier. Chaque requête avec `capakeys` effectue une consultation du cadastre plus une analyse parcellaire par parcelle (jusqu'à 10) ; une ferme avec 3 parcelles prend 3 à 12 s ; la première analyse d'une parcelle dont les couches régionales sont froides peut prendre jusqu'à 21 s (le budget de la section parcel). Quand ce budget est dépassé, la réponse reste 200, la section parcel manque (`upstream_errors.parcel: timeout`, message `section_missing`) ; le pack de base est facturé une fois : redemandez après quelques secondes avec une nouvelle `Idempotency-Key` ; la répétition est gratuite (dédoublonnage) et reconstruit la section parcel.

## 4. Carte des prix (section `price_map`)

Le pack de base (`sections=basic`) inclut la carte des prix : les niveaux des prix demandés par quartier dans un rayon de 3 000 m autour de l'adresse (au plus 40 quartiers) sous forme de **données plus géométrie GeoJSON**. Taxon ne fournit ni images ni tuiles cartographiques ; vous dessinez les polygones vous-même avec votre propre bibliothèque et licence cartographiques (Leaflet, MapLibre, Mapbox, ...) et les colorez selon `price_per_m2_house` ou `price_per_m2_apartment`.

```bash
curl -s -G "https://taxonapi.be/api/v1/partner/address" \
  -H "X-Api-Key: $TAXON_API_KEY" -H "X-User-Ref: ag-0417" -H "X-Case-Ref: PT-2026-004512" -H "Accept-Language: en" \
  --data-urlencode "address=Avenue Alphonse Allard 94, 1420 Braine-l'Alleud" \
  --data-urlencode "sections=basic" --data-urlencode "type=house" \
  | jq '.price_map | {radius_m, price_kind, reference_period, updated_at, subject_neighbourhood, n: (.neighbourhoods | length), first: (.neighbourhoods[0] | del(.geometry)), source}'
```

Réponse réelle du 07-09-2026 (`request_id` 88915700832ddc745b4355de3c15f332, 0,3 s ; message `price_map_sparse` parce que le quartier du bien ne compte que 5 annonces) :

```json
{
  "radius_m": 3000,
  "price_kind": "asking_price",
  "reference_period": "2026-03-30",
  "updated_at": "2026-07-09T10:26:08+00:00",
  "subject_neighbourhood": "5906",
  "n": 40,
  "first": {"id": "5906", "name": "Saint-Sebastien", "municipality": "Braine-l'Alleud", "postal_code": "1420", "distance_m": 0, "is_subject": true,
            "price_per_m2_house": 2466.07, "price_per_m2_apartment": 2912.64, "listings_count_house": 3, "listings_count_apartment": 2, "low_sample": true},
  "source": "Taxon asking prices (advertised prices, not notarial sale prices)"
}
```

Forme du bloc : `{radius_m: 3000, price_kind: "asking_price", reference_period (date of the price layer), updated_at (last rebuild of that layer), subject_neighbourhood (id string of the neighbourhood that contains the address), neighbourhoods[] (sorted by distance, at most 40, the subject neighbourhood always included), source}` ; par quartier `{id, name, municipality, postal_code, distance_m, is_subject, price_per_m2_house, price_per_m2_apartment (asking-price level per m² from the Taxon price layer of reference_period, null when the layer has no figure), listings_count_house, listings_count_apartment (number of Taxon listings of the last 24 months in that neighbourhood: a measure of how much current supply supports the figure, not the sample behind it; 0 = a reference-layer figure without recent Taxon listings), low_sample (fewer than 30 such listings), geometry}` où `geometry` est un Polygon ou MultiPolygon GeoJSON (WGS84 lon/lat). Taille du bloc de 20 à 50 Ko ; réponse à chaud sous 0,5 s, la première requête après un redémarrage du service peut prendre jusqu'à 20 s. Le dessiner avec Leaflet :

```js
L.geoJSON(bundle.price_map.neighbourhoods.map(n => ({type: "Feature", geometry: n.geometry, properties: n})), {
  style: f => ({fillColor: colour(f.properties.price_per_m2_house), weight: f.properties.is_subject ? 3 : 1,
                dashArray: f.properties.low_sample ? "4 4" : null, fillOpacity: 0.5})
}).bindPopup(l => `${l.feature.properties.name}: ${l.feature.properties.price_per_m2_house ?? "n/a"} EUR/m²`).addTo(map);
```

Règles : `low_sample: true` = moins de 30 annonces Taxon des 24 derniers mois dans ce quartier, à présenter comme indicatif (par exemple hachuré) ; message `price_map_sparse` (section `price_map`) lorsque le quartier du bien lui-même compte moins de 30 annonces, la section est tout de même fournie ; lorsqu'aucun quartier avec des prix ne se trouve dans un rayon de 3 km, la section vaut `null`, le message `price_map_unavailable` est ajouté, `upstream_errors.price_map` vaut `no_coverage` ; pas une panne, le pack de base reste facturé. Affichez la ligne `source` à côté de la carte. La géométrie relève des mêmes règles d'utilisation que le reste du paquet (par dossier, pas de couche cartographique dérivée). La carte des prix fait partie du pack de base : pas de prix distinct, le prix du pack figure dans `billing.price.package` et dans la colonne CSV `price_package` ; le dédoublonnage suit le pack.

## 5. Photos

Chaque point de référence porte au plus 5 objets `photos[] {url, download_token, valid_until}`, photo principale (façade) en premier, liste vide sans photos.

- `url` est une URL signée (capability URL) sur taxonapi.be, actuellement `https://taxonapi.be/api/v1/marketexplorer/foto/<token>?w=640`. `w` est la largeur côté serveur : 160, 320 ou 640 (les autres valeurs sont arrondies vers le haut et plafonnées à 640 ; sans `w`, le lien livre aussi 640 px), toujours en JPEG ; l'original n'est jamais servi. Elle fonctionne sans clé, directement dans un `<img>`, avec `Cache-Control: public, max-age=1800, immutable`. Ne pas l'analyser, ne pas la composer vous-même, la charger dans l'heure.
- `valid_until` (UTC) est 1 heure après l'émission. Après expiration ou en cas de manipulation, l'URL répond HTTP 404 avec un corps `text/plain` `invalid_or_expired_token` (mesuré en réel : un token modifié donne immédiatement 404).
- Lors d'une demande répétée (dédoublonnage ou rejeu par Idempotency-Key), les liens photo sont renouvelés : nouveaux `url` et `valid_until`, même `download_token`. Redemandez donc le paquet au lieu de stocker les liens.
- `download_token` est un identifiant opaque stable (20 hex) pour votre propre comptabilité ou dédoublonnage ; il ne récupère rien.
- **Intégration dans le rapport (2.5.0).** Récupérez les photos pendant la validité du lien (1 heure) et ne les utilisez que pour le dossier pour lequel la requête a été faite. Les intégrer dans le rapport d'expertise (PDF) de ce dossier est autorisé, toujours avec une mention de source visible à côté de chaque photo (l'`attribution` de la réponse, par exemple « Références : Taxon (taxon.be) »), sous réserve de la convention de partenariat (le partenaire garantit Taxon contre les réclamations de portails ou d'agents immobiliers concernant les photos). Pas de republication, pas de conservation en dehors de ce rapport, pas de téléchargement en masse ; la résolution reste limitée à 640 px (`w=640`). Récupérez l'image côté serveur au moment de composer le rapport ; ne conservez pas le lien, il expire après une heure.

**Caractéristiques, résumé et vignette (2.2.0).** Chaque point de référence porte aussi `features` (caractéristiques structurées de l'annonce : garage, parking_spaces, terrace et terrace_m2, garden et garden_m2, cellar, attic, floor, floors_count, elevator, kitchen, bathrooms, shower_rooms, toilets, heating, solar_panels, double_glazing, orientation_garden, renovation_year, inspections {electrical_compliant, asbestos_certificate, oil_tank}, flood_zone ; chaque clé présente, `null` = inconnu, valeurs d'énumération en anglais dans toutes les langues), `summary` (une phrase dans les mots de Taxon, construite uniquement à partir de ces champs, dans la langue de `Accept-Language` ; jamais de texte d'annonce) et `photo_count` (nombre total de photos de l'annonce, `photos[]` reste limité à 5). `thumbnail {url, valid_until}` est la photo principale (façade) en `w=160` sous les mêmes règles que les liens photo (1 heure, renouvelée lors d'une demande répétée, ne pas stocker) ; `photos[0]` est la même photo en 640 px ; `null` sans photos. Affichez `summary` comme ligne d'introduction et `features` sous forme de puces ou d'un petit tableau ; traitez `null` comme « non renseigné », jamais comme « non ». Exemple : `"summary": "Maison mitoyenne de 156 m² sur un terrain de 259 m² avec garage, jardin (190 m²), terrasse (34 m²) et cave, 3 chambres, PEB B, construite en 1975."` avec `"features": {"garage": true, "parking_spaces": 3, "terrace": true, "terrace_m2": 34, "garden": true, "garden_m2": 190, "cellar": true, "attic": true, "floor": null, "floors_count": 4, "elevator": false, "kitchen": "equipped", "bathrooms": 1, "shower_rooms": null, "toilets": 2, "heating": "gas", "solar_panels": null, "double_glazing": true, "orientation_garden": null, "renovation_year": null, "inspections": {"electrical_compliant": false, "asbestos_certificate": null, "oil_tank": null}, "flood_zone": "none"}`, `"photo_count": 30` (point de référence réel du 07-09-2026).

## 6. Langues

`Accept-Language: nl` (par défaut), `fr` ou `en` fixe la langue des libellés (`type_label`, `transaction_label`, `price_kind_label`, `status_label`, `source_label`, `history[].label`, `condition` d'un point de référence, `epc_source`, `building_stock.distribution[].label`, libellés d'`amenities`, `confidence.label`), des messages et des messages d'erreur, d'`attribution` et de `disclaimer`, des lignes de source (`source`) et du nom de la commune (une seule règle pour `address`, les points de référence, `neighbourhood.sector`, `neighbourhood.safety` et `price_map` : Flandre en néerlandais, Wallonie en français, Bruxelles en néerlandais pour `nl` et en français pour `fr` et `en` ; pas d'exonymes, donc Liège et Braine-l'Alleud aussi pour `nl`). Les clés JSON et les valeurs d'énumération (`sale`, `rent`, `asking_price`, `house_number`, `terraced`, `published`, ...) sont toujours en anglais. Le `label` d'affectation est le texte du service régional (néerlandais pour VL, français pour WAL) et n'a pas de traduction anglaise ; `zoning.category` est une valeur d'énumération anglaise (`residential`, `residential_rural`, `residential_expansion`, `agricultural`, `industrial`, `nature`, `forest`, `park`, `recreation`, `community_facilities`, `extraction`, `weekend_residence`, `mixed`, `other`). Les points cardinaux (`garden_orientation`, `parcel_candidates[].direction`, `features.orientation_garden`) sont indépendants de la langue (`N`, `NE`, `E`, ...). L'ordre de `notices[]` n'a pas de signification. Tous les exemples de cette documentation envoient `Accept-Language: en` (sortie identique dans les trois langues) ; avec `fr` vous recevez libellés, messages et noms de commune en français (voir T07).

Mesuré en réel sur la même adresse wallonne (07-09-2026) : `transaction_label` for sale / à vendre / te koop ; `confidence.label` medium / moyenne / gemiddeld ; `garden_orientation` SW dans chaque langue ; `address.municipality` Braine-l'Alleud dans chaque langue (pas d'exonyme) ; `attribution` "References: Taxon (taxon.be)" / "Références : Taxon (taxon.be)" / "Referenties: Taxon (taxon.be)".

## 7. PHP (cURL, PHP 8)

```php
<?php
final class TaxonPartnerClient
{
    private const BASE = 'https://taxonapi.be/api/v1/partner/';

    public function __construct(private string $apiKey, private string $lang = 'en') {}

    /** @return array{status:int, headers:array<string,string>, body:array} */
    public function address(string $address, string $type, array $sections, string $userRef,
                            ?string $caseRef = null, array $extra = [], ?string $idemKey = null): array
    {
        $query = array_merge(['address' => $address, 'type' => $type, 'sections' => implode(',', $sections)], $extra);
        $headers = [
            'X-Api-Key: ' . $this->apiKey,
            'X-User-Ref: ' . $userRef,
            'Accept-Language: ' . $this->lang,
            'Idempotency-Key: ' . ($idemKey ?? bin2hex(random_bytes(16))),
        ];
        if ($caseRef !== null) { $headers[] = 'X-Case-Ref: ' . $caseRef; }
        return $this->get('address?' . http_build_query($query), $headers);
    }

    public function usage(string $month, bool $csv = false): array
    {
        $q = http_build_query(['month' => $month, 'format' => $csv ? 'csv' : 'json']);
        return $this->get('usage?' . $q, ['X-Api-Key: ' . $this->apiKey], !$csv);
    }

    private function get(string $path, array $headers, bool $json = true): array
    {
        $respHeaders = [];
        $ch = curl_init(self::BASE . $path);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT        => 60,          // AVM + parcel can take 10-25 s
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$respHeaders) {
                if (str_contains($line, ':')) { [$k, $v] = explode(':', $line, 2); $respHeaders[strtolower(trim($k))] = trim($v); }
                return strlen($line);
            },
        ]);
        $raw = curl_exec($ch);
        if ($raw === false) { throw new RuntimeException('Taxon: ' . curl_error($ch)); }
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);
        $body = $json ? (json_decode($raw, true) ?? []) : ['csv' => $raw];
        return ['status' => $status, 'headers' => $respHeaders, 'body' => $body];
    }
}

// Use with retry on 429/503/5xx (same Idempotency-Key!)
$client = new TaxonPartnerClient(getenv('TAXON_API_KEY'), 'en');
$idem = bin2hex(random_bytes(16));
for ($attempt = 1; $attempt <= 3; $attempt++) {
    $r = $client->address("Avenue Alphonse Allard 94, 1420 Braine-l'Alleud", 'house', ['basic', 'avm'],
                          'ag-0417', 'PT-2026-004512', ['living_area_m2' => 165, 'year_built' => 1978, 'epc' => 'D', 'condition' => 'good'], $idem);
    if ($r['status'] === 200) { break; }
    if ($r['status'] === 429 || $r['status'] === 503) { sleep((int)($r['headers']['retry-after'] ?? $r['body']['retry_after'] ?? 5)); continue; }
    if ($r['status'] >= 500) { sleep(2 ** $attempt); continue; }
    throw new RuntimeException("Taxon {$r['status']} {$r['body']['error']} ({$r['body']['request_id']}): {$r['body']['message']}");
}
$bundle = $r['body'];
// Must be shown in the UI and in the report:
echo $bundle['attribution'], "\n", $bundle['disclaimer'], "\n";
echo 'Charged: ', $bundle['billing']['charged'] ? 'yes' : 'no (' . ($bundle['billing']['free_reason'] ?? '-') . ')', "\n";
foreach ($bundle['notices'] as $n) { echo 'notice: ', $n['code'], ' ', $n['section'] ?? '-', ' ', $n['message'], "\n"; }
foreach ($bundle['basic']['comparables'] as $block) foreach ($block['items'] as $c) {
    printf("%s %s | %s | %d m | %s EUR (%s) | %d m² | EPC %s\n", $block['transaction'], $block['type'],
        $c['address'], $c['distance_m'], number_format($c['price'], 0, ',', '.'), $c['price_kind'], $c['living_area_m2'], $c['epc_label'] ?? '-');
}
```

## 8. Python (requests)

```python
import os, time, uuid, requests

BASE = "https://taxonapi.be/api/v1/partner/"
KEY = os.environ["TAXON_API_KEY"]          # tx_test_... during the pilot tests

def taxon_address(address, type_, sections=("basic",), user_ref="ag-0417", case_ref=None, lang="en", **extra):
    headers = {
        "X-Api-Key": KEY,
        "X-User-Ref": user_ref,
        "Accept-Language": lang,
        "Idempotency-Key": str(uuid.uuid4()),   # reused on every retry below
    }
    if case_ref:
        headers["X-Case-Ref"] = case_ref
    params = {"address": address, "type": type_, "sections": ",".join(sections), **extra}
    for attempt in range(1, 4):
        r = requests.get(BASE + "address", headers=headers, params=params, timeout=(5, 60))
        if r.status_code == 200:
            return r.json()
        if r.status_code in (429, 503):
            body = r.json() if r.headers.get("Content-Type", "").startswith("application/json") else {}
            time.sleep(int(r.headers.get("Retry-After") or body.get("retry_after") or 5)); continue
        if r.status_code >= 500:
            time.sleep(2 ** attempt); continue
        err = r.json()
        raise RuntimeError(f"Taxon {r.status_code} {err['error']} ({err['request_id']}): {err['message']}")
    raise RuntimeError("Taxon: no answer after 3 attempts")

bundle = taxon_address("Avenue Alphonse Allard 94, 1420 Braine-l'Alleud", "house",
                       sections=("basic", "avm"), case_ref="PT-2026-004512",
                       living_area_m2=165, year_built=1978, epc="D", condition="good")

print(bundle["attribution"]); print(bundle["disclaimer"])            # must be shown
print("sections:", bundle["sections_delivered"], "| charged:", bundle["billing"]["charged"])
for n in bundle["notices"]:
    print("notice:", n["code"], n.get("section"), n["message"])
if bundle.get("avm"):
    a = bundle["avm"]
    print(f"AVM {a['value']:,} EUR, range {a['range']}, confidence {a['confidence']['label']}")
for section, reason in bundle["upstream_errors"].items():
    print("failure (not charged):", section, reason)

# Usage as CSV
csv = requests.get(BASE + "usage", headers={"X-Api-Key": KEY},
                   params={"month": "2026-09", "format": "csv"}, timeout=30)
open("taxon-usage-2026-09.csv", "wb").write(csv.content)
```

## 9. Gestion des erreurs

Chaque erreur a le même corps : `{"error", "message", "request_id", "charged": false}` plus, selon le code, `details[] {field, issue}`, `retry_after`, `scope`, `limit`, `section`, `user_ref`, `reason`, `since` ou `pilot`. `message` suit `Accept-Language`. Aucune erreur n'est jamais facturée. Les erreurs d'en-tête et de paramètre sont signalées ensemble dans un seul `400 invalid_request` (`details[]` liste chaque champ erroné, en-têtes compris).

| HTTP | error | Signification | Ce que fait le client |
|---|---|---|---|
| 400 | `invalid_request` | Paramètre ou en-tête invalide ; `details[]` nomme le champ (aussi un nom de paramètre 1.x tel que `adres`, un paramètre envoyé deux fois, une section inconnue, `capakeys` sans la section parcel, un `X-User-Ref`, `X-Case-Ref` ou `Idempotency-Key` invalide) | Corriger la requête ; ne jamais réessayer telle quelle |
| 400 | `user_ref_required` | `X-User-Ref` manquant (et aucun alias) | Ajouter l'en-tête |
| 400 | `user_ref_conflict` | Deux en-têtes utilisateur ou plus avec des valeurs différentes | Envoyer un seul en-tête, de préférence `X-User-Ref` |
| 401 | `missing_api_key`, `invalid_api_key` | Clé manquante ou inconnue | Erreur de configuration ; alerter l'exploitation, ne pas réessayer |
| 403 | `key_revoked` | Clé révoquée | Passer à la nouvelle clé |
| 403 | `ip_not_allowed` | IP hors de la liste autorisée | Communiquer la nouvelle IP du serveur à Taxon |
| 403 | `client_suspended` | Client suspendu | Contacter Taxon ; ne pas réessayer |
| 403 | `section_not_allowed` | Section (`section`) hors de votre plan | Retirer la section |
| 403 | `quota_exceeded` | Plafond journalier ; `scope` `day` (clé, `limit` 300 ; clé de test 20) ou `user_day` (`user_ref`, `limit` 20) ; en-tête `Retry-After` = secondes jusqu'à minuit Europe/Brussels | Mettre en file d'attente jusqu'à `Retry-After` ; les autres utilisateurs continuent de fonctionner |
| 403 | `pilot_exhausted` | Quota ou période du pilote épuisé ; `pilot{}` | Contacter Taxon |
| 403 | `user_suspended` | Utilisateur suspendu (`reason` `raster_scan`, `handmatig` ou texte, `since`) ; pas de Retry-After, pas d'expiration automatique | Bloquer cet utilisateur dans votre interface ; levée via info@taxon.be avec `user_ref` |
| 404 | `address_not_found` | Aucun géocodeur ne trouve l'adresse | Laisser l'utilisateur corriger l'adresse |
| 404 | `not_found` | Chemin inconnu (aussi les chemins 1.x `/adres`, `/verbruik`) | Corriger le chemin |
| 405 | `method_not_allowed` | Autre que GET | Utiliser GET |
| 409 | `idempotency_conflict` | Même `Idempotency-Key` pour une autre requête | Utiliser une nouvelle clé |
| 422 | `address_imprecise` | Trouvée uniquement au niveau de la rue ou de la commune, ou sans code postal ni commune | Demander le numéro de maison et le code postal |
| 422 | `type_unsupported` | Type non pris en charge pour cette adresse | Changer le type |
| 422 | `capakey_invalid` | Format ou plus de 10 (`details[]`) | Corriger les capakeys |
| 422 | `capakey_too_far` | Une parcelle à plus de 2 km | Retirer cette parcelle |
| 429 | `rate_limited` | Plafond par minute (application) ; en-tête `Retry-After` et `retry_after` | Attendre, réessayer avec la même `Idempotency-Key` |
| 429 | `rate_limit_exceeded` | Plafond par minute (nginx) ; corps `{"error": "rate_limit_exceeded", "retry_after": 60}`, pas d'en-tête, pas de `request_id` | Attendre le `retry_after` du corps, réessayer |
| 502 | `upstream_unavailable` | La section basic n'a pas pu être fournie ; `details[]` nomme le bloc | Jusqu'à 3 tentatives avec attente exponentielle (2, 4, 8 s), même `Idempotency-Key` |
| 503 | `overloaded` | Protection contre la surcharge ; `Retry-After` | Attendre `Retry-After`, réessayer avec la même `Idempotency-Key` |
| 504 | `timeout` | Paquet plus long que 40 s | Réessayer avec la même `Idempotency-Key` |
| 500 | `internal_error` | Inattendu | Réessayer une fois, puis signaler le `request_id` |

Règles que vous appliquez dans le code :

- Délai d'attente du client d'au moins 60 s ; réessayer uniquement sur 429 (avec `Retry-After` ou `retry_after`), 503 et 5xx (502/504/500), toujours avec la même `Idempotency-Key` ; reconnaître une réponse rejouée à `X-Idempotent-Replay: true` ; la même clé avec toute autre entrée (adresse, utilisateur, sections, paramètres AVM, `capakeys` ou langue) donne 409 `idempotency_conflict` ; `X-Request-ID` n'est pas une clé d'idempotence (nginx la réécrit). Sur `403 quota_exceeded`, attendre `Retry-After`.
- Ignorer les champs JSON inconnus et les codes de message inconnus ; gérer les sections manquantes (`parcel`, `avm`, `price_map`) via `sections_delivered`, `notices[]` (volontairement non fournies : `living_area_required`, `parcel_not_found`, `avm_insufficient_data`, `price_map_unavailable`) et `upstream_errors` (panne `timeout` ou `upstream_error`, message `section_missing` ; pour `price_map` aussi `no_coverage`). La facturation suit le pack (2.4.0) : une `parcel` ou `price_map` manquante laisse le pack de base facturé, une `avm` manquante n'est pas facturée. `basic.comparables` est une liste de blocs par type et transaction (`sale`, `rent`) ; un sous-bloc de `neighbourhood` peut valoir `null`. Sur `upstream_errors.parcel: timeout` (analyse parcellaire à froid), redemandez après quelques secondes avec une nouvelle `Idempotency-Key` ; le pack de base n'est alors pas refacturé (dédoublonnage) et la section manquante est reconstruite gratuitement.
- Parcelles : afficher `parcel.parcels[]` et `total_area_m2` dans le rapport lorsque des `capakeys` ont été envoyées, pas seulement les champs au niveau de la section de la parcelle principale ; `parcel_candidates[]` est une liste de choix pour l'utilisateur, pas une déclaration de propriété. Ne jamais envoyer de `capakeys` d'un autre dossier et écarter les candidates que l'utilisateur n'a pas cochées avant l'étape 2.
- Afficher `attribution` et `disclaimer` de la réponse à côté des données et dans le rapport ; `amenities.attribution` à côté des équipements ; `source` à côté de la parcelle, du parc immobilier, de la sécurité et de la carte des prix.
- Supprimer les réponses brutes après au plus 30 jours (tâche planifiée) ; seul le rapport finalisé reste dans l'archive du dossier.
- Les balayages systématiques (numéros de maison ou codes postaux consécutifs en peu de temps) suspendent l'utilisateur : `403 user_suspended` avec `user_ref`, `reason` et `since`, pas de Retry-After, pas d'expiration automatique ; levée uniquement par Taxon (info@taxon.be) ; la suspension frappe l'utilisateur, pas le client ; 3 utilisateurs suspendus ou plus en 24 h = `client_suspended` pour toute la clé. Ne construisez donc jamais de boucle en masse ou de test sur des adresses réelles en dehors du plan de test.
- `X-User-Ref` = le véritable identifiant de l'utilisateur, stable, jeu de caractères `A-Z a-z 0-9 . _ : @ -` (au plus 64 ; l'API normalise en minuscules) ; jamais une valeur fixe pour tous les utilisateurs (c'est un abus du dédoublonnage et cela mène à la suspension).
- Journaliser par requête `request_id`, utilisateur, dossier, `sections_delivered`, `billing.charged`, `billing.price.package`, `billing.price.avm`, `billing.price.total` : votre comptabilité correspond alors à `GET /usage` et à la facture mensuelle.

## 10. Idempotence

Envoyez un UUID frais en `Idempotency-Key` avec chaque requête et ne le réutilisez que pour les nouvelles tentatives de cette même requête. La même clé dans les 24 heures renvoie la réponse conservée octet pour octet (même `request_id`, en-tête de réponse `X-Idempotent-Replay: true`) sans nouvelle facturation et sans comptage vis-à-vis du plafond journalier ; les liens photo du rejeu reçoivent une nouvelle validité. Le rejeu couvre les réponses 200 conservées ; après une erreur (4xx ou 5xx), la même clé exécute simplement la requête à nouveau (c'est ce qu'une nouvelle tentative après 502/503/504 exige), et chaque tentative exécutée compte pour les plafonds journaliers comme décrit à la section 1. La même clé avec une autre adresse, un autre utilisateur, une autre liste de sections, un autre paramètre AVM, d'autres `capakeys` ou une autre langue donne `409 idempotency_conflict` (non facturé) : utilisez une nouvelle clé. Mesuré en réel le 08-09-2026 : le rejeu de l'exemple principal (Doorniksestraat 40, Kortrijk) a renvoyé `request_id` df0cfcebf101496f99e7f01a0f14f227 avec `X-Idempotent-Replay: true` en 80 ms, corps identique octet pour octet ; la même clé avec une autre adresse a donné 409 `idempotency_conflict` (non facturé).

Le dédoublonnage est un mécanisme différent (commercial, 30 jours, par utilisateur et bien, par pack ou option) et fonctionne sans aucun en-tête : le même utilisateur qui redemande le même bien reçoit `from_cache: true` et `free_reason: dedup` lorsque toutes les sections demandées avaient déjà été fournies, ou des messages `dedup` par section lorsque seules certaines l'avaient été. Un bien est le point géocodé (arrondi à environ 1 m) plus le numéro de boîte : un autre `type`, d'autres paramètres AVM, d'autres `capakeys` ou une autre langue ne font pas un autre bien. Le dédoublonnage ne fige rien : une telle répétition est reconstruite avec la nouvelle entrée (libellés frais, `inputs_used` frais) et reste gratuite pour les sections déjà fournies ; `free_reason` vaut `dedup` dès que chaque section fournie l'avait déjà été (réponse du grand livre ou reconstruction, aussi avec une clé de test), sinon `test`, `pilot` ou `null` (facturé) avec des messages `dedup` par section. Une requête sans résultat (zéro point de référence, `free_reason: no_result`) ne démarre aucune fenêtre de dédoublonnage : la requête suivante pour le même bien par le même utilisateur est reconstruite et facturée normalement dès que des points de référence existent (une option AVM fournie à ce moment-là garde bien sa propre fenêtre).

## 11. Plan de test du pilote (clé de test tx_test_)

Clé de test : au plus 20 requêtes par jour, jamais facturée, la réponse contient `"environment": "test"` et un message `test_environment`. Répartissez les tests sur quelques jours ou demandez un plafond de test temporairement plus élevé. Les 20 se comptent comme décrit à la section 1 (accès dédoublonnés et erreurs d'adresse 404/422 compris ; rejeux par Idempotency-Key non). Les valeurs attendues ci-dessous ont été mesurées en réel le 07-09-2026 avec une clé de test.

### Adresses de test (3 par région)

| Région | Adresse | type | living_area_m2 | Attendu |
|---|---|---|---|---|
| WAL | Avenue Alphonse Allard 94, 1420 Braine-l'Alleud (vérifiée par Taxon) | house | 165 | region WAL, nis_code 25014, capakey 25744E0226/00_000, zoning plan "plan de secteur" label "Habitat", garden_orientation SW, message avm_indicative_not_regionally_calibrated |
| WAL | Rue de Namur 23, 1300 Wavre | house | 140 | region WAL, nis_code 25112 |
| WAL | Boulevard Tirou 50, 6000 Charleroi | apartment | 85 | region WAL, nis_code 52011, comparables type apartment |
| BXL | Avenue Louise 200, 1050 Ixelles | apartment | 110 | region BXL, zoning plan GBP/PRAS, message avm_indicative_not_regionally_calibrated |
| BXL | Rue Royale Sainte-Marie 22, 1030 Schaerbeek (vérifiée par Taxon) | apartment | 95 | region BXL, nis_code 21015, capakey 21908E0259/00N000, garden_orientation null, avm.inputs_used.plot_area_m2 null |
| BXL | Avenue de Tervueren 150, 1150 Woluwe-Saint-Pierre | house | 220 | region BXL, nis_code 21019 |
| VL | Rijselstraat 60, 8900 Ieper (vérifiée par Taxon) | house | 150 | region VL, nis_code 33011, geocoder geo.api.vlaanderen.be, zoning plan gewestplan, pas de message indicatif |
| VL | Kortrijksesteenweg 300, 9000 Gent | apartment | 90 | region VL, nis_code 44021 |
| VL | Mechelsesteenweg 100, 2018 Antwerpen | apartment | 75 | region VL, nis_code 11002 |

Trois adresses (une par région) ont été vérifiées par Taxon contre BeSt ; les autres servent à tester le géocodage, la détection de la région et la logique des sections. Lorsqu'un numéro de maison n'existe pas dans BeSt (404 `address_not_found`), prenez un numéro voisin dans la même rue (la facturation est de toute façon nulle en test).

### Scénarios (à cocher)

| # | Scénario | Appel | Attendu |
|---|---|---|---|
| T01 | Health | `GET /health` sans clé | 200, `status: ok`, `versie: "2.5.1"` (version du service, contrat 2.0), `bundel: live`, `db: ok` ; `POST /address` = 405 `method_not_allowed` ; chemin inconnu et chemins 1.x `/adres`, `/verbruik` = 404 `not_found` |
| T02 | Sans clé | `GET /address` sans `X-Api-Key` | 401 `missing_api_key` |
| T03 | Mauvaise clé | `X-Api-Key: tx_test_0000...` | 401 `invalid_api_key`, même temps de réponse que T02 |
| T04 | Sans utilisateur | clé valide, sans `X-User-Ref` (et sans alias) | 400 `user_ref_required` (message dans la langue d'`Accept-Language`) |
| T04b | Alias obsolète | uniquement `X-Gebruiker-Ref: ag-0417` ou `X-Kantoor-Ref: AG-0417` | 200, `billing.user_ref: "ag-0417"` (l'alias fonctionne, même normalisation) |
| T04c | Conflit d'alias | `X-User-Ref: ag-0417` et `X-Kantoor-Ref: ag-0022` | 400 `user_ref_conflict`, `details[0].field: "X-User-Ref"`, non facturé ; les deux identiques (aussi `AG-0417`) = 200 |
| T05 | Validation | `address=abc` (trop court), `type=villa`, `living_area_m2=7`, `X-User-Ref: ag 04/17`, ancien nom `adres=` | 400 `invalid_request` avec `details[]` (tous les champs erronés en une seule réponse, en-têtes compris ; `field` = `address`, `type`, `living_area_m2`, `X-User-Ref`, ou `adres` avec issue "Extra inputs are not permitted" ; `:` et `@` sont autorisés dans la référence utilisateur) |
| T06 | Basic WAL | Braine-l'Alleud, `sections=basic` | 200, `sections_delivered: ["basic", "parcel", "price_map"]` (pack de base), `comparables` = liste de blocs (sale et rent, chacun avec `address_mode`) avec <= 25 éléments, chaque `price_kind = asking_price`, `source = listing`, `epc_source = "as advertised"`, pas de nom de portail ni de lien, `safety` avec chiffres pour 1 000, `building_stock.distribution` avec catégories residential/commerce_services/industry/agriculture/other, `upstream_errors: {}`, `environment: test` |
| T07 | Libellés FR | idem T06 avec `Accept-Language: fr` | `attribution = "Références : Taxon (taxon.be)"`, clause de non-responsabilité en français, messages en français, `transaction_label: "à vendre"`, `confidence.label: "moyenne"` (avec avm), `garden_orientation: "SW"` (avec parcel) |
| T07b | Libellés NL | idem avec `Accept-Language: nl` | `attribution = "Referenties: Taxon (taxon.be)"`, `address.municipality: "Braine-l'Alleud"`, `transaction_label: "te koop"` |
| T08 | AVM sans surface habitable | `sections=basic,avm` sans `living_area_m2` | 200, `avm` absente de `sections_delivered`, message `{code: living_area_required, section: avm}`, `billing.price.avm` = 0 |
| T09 | AVM avec surface habitable | `sections=basic,avm&living_area_m2=165&year_built=1978&epc=D&condition=good` | 200, `avm.value`, `range`, `range_90`, `rental_value_range`, `confidence`, `price_basis: asking_price_model`, `inputs_used.condition: "good"` ; message `avm_indicative_not_regionally_calibrated` (WAL/BXL), pas pour VL |
| T10 | Parcelle par région | `sections=basic` sur une adresse par région (parcel fait partie du pack de base) | `capakey` rempli, `cadastral_area_m2`, `fiscal_situation` (2.4.2, p. ex. `2027-01-01`), `zoning.plan` = plan de secteur / GBP/PRAS / gewestplan ou RUP, `preemption_right.status` (`none`, `yes` ou `unknown`), `source` rempli, pas de champ inondation, pas de géométrie ; `main_parcel`, `parcels[]` (1 élément), `parcel_candidates[]` (<= 12, chacune avec `direction` N/E/S/W et `built`), `parcel_candidates_remark: null` |
| T10b | Plusieurs parcelles (maison mitoyenne + garage) | `address=Rijselstraat 62, 8900 Ieper&sections=basic,avm&type=house&living_area_m2=140&bedrooms=3&capakeys=33011I0172/00H000,33011I0172-00G000` | 200, `parcels_count: 2`, `total_area_m2: 669.02`, `main_parcel: 33011I0172/00H000`, `parcels[1].distance_to_address_m: 26`, `parcel_candidates: null`, `avm.inputs_used.plot_area_m2: 669.02` et `plot_area_source: "cadastre_2_parcels"`, `bedrooms_filter.applied: true` ; demandé par le même utilisateur après l'étape 1 de la section 3 sur Rijselstraat 62 : message `dedup` pour `basic` (pack de base), `free_reason: test` (clé de test ; `avm` est nouvelle ici, avec une clé de production seule `avm` est facturée et `free_reason` vaut `null` ; `dedup` uniquement quand rien de nouveau n'est fourni ; T10 lui-même utilise Rijselstraat 60, un autre bien) |
| T10c | Ferme avec 3 parcelles | `address=Dikkebusstraat 5, 8954 Heuvelland&sections=basic&type=house&capakeys=33015C0813/00K000,33015C0812/00L000,33015C0795/00D000` | `parcels_count: 3`, `total_area_m2: 16058.83`, `zoning_combined` = un objet (category `agricultural`, label Landbouwgebied, plan gewestplan), `preemption_right_combined.status: none`, `plot_area_source: cadastre_3_parcels`, durée < 15 s (mesurée 4,4 s) |
| T10d | Chemins d'erreur capakey | (1) `capakeys=FOUT` ; (2) 11 clés ; (3) `capakeys=33011I0172/00H000,33015C0813/00K000` sur Rijselstraat 62 Ieper ; (4) `capakeys=33011I0172/00H000,33011I9999/00Z000` ; (5) `capakeys=...` avec `sections=basic` | (1) et (2) 422 `capakey_invalid` avec `details[]` (issue "invalid: FOUT" resp. "11 given, maximum 10") ; (3) 422 `capakey_too_far` ("lies 7474 m from the address"), non facturé ; (4) 200 avec message `capakey_not_found` et `parcels_not_found: ["33011I9999/00Z000"]`, 1 parcelle fournie ; (5) 200, parcelle fournie (la section parcel fait partie du pack de base) |
| T10e | plot_area_m2 et bedrooms | T09 avec `&plot_area_m2=850&bedrooms=3` (sans capakeys) | `avm.inputs_used.plot_area_m2: 850.0`, `plot_area_source: "partner"`, message `plot_area_not_cadastral` ; champ de bloc `bedrooms_filter` (`applied` true avec >= 8 points de référence de 2 à 4 chambres, sinon message `bedrooms_filter_dropped`) ; `inputs_used.bedrooms: 3` |
| T10f | Carte des prix | `sections=basic` (carte des prix dans le pack de base) sur Braine-l'Alleud, Ieper (Rijselstraat 60) et Schaerbeek | 200, `price_map` dans `sections_delivered`, `radius_m: 3000`, `price_kind: asking_price`, `reference_period: "2026-03-30"`, `subject_neighbourhood` = l'`id` de l'élément avec `is_subject: true` (Braine-l'Alleud "5906" Saint-Sebastien, Ieper "7220" Ieper-Centrum, Schaerbeek "3110"), `neighbourhoods` 40 / 38 / 40 éléments, chacun avec `geometry.type` Polygon ou MultiPolygon, exactement un `is_subject: true` ; Braine-l'Alleud porte le message `price_map_sparse` ; `billing.price.package` présent ; ou `price_map: null` avec message `price_map_unavailable` et `upstream_errors.price_map: no_coverage` (pack inchangé) |
| T11 | Dédoublonnage, trois clics | T06 trois fois avec le même `X-User-Ref` | 1re : `free_reason: "test"` (clé de test, prix 0), 2e et 3e : `from_cache: true`, `free_reason: "dedup"`, `dedup_of` = request_id de la 1re, message `dedup` avec `section: null` |
| T12 | Dédoublonnage section supplémentaire | après T11 : `sections=basic,avm&living_area_m2=165` | uniquement `price.avm` > 0 (production) ou marquée comme nouvelle ; `price.package` = 0 et message `dedup` avec `section: basic` (pack de base déjà fourni) |
| T13 | Autre utilisateur | T06 avec `X-User-Ref: ag-0022` | `dedup_of: null`, nouvelle facturation |
| T14 | Idempotence | T09 deux fois avec la même `Idempotency-Key` | corps identique, même `request_id`, la seconde porte l'en-tête `X-Idempotent-Replay: true` et ne compte pas dans `billing.today` |
| T14b | Conflit d'idempotence | même `Idempotency-Key` que T14 avec une autre adresse | 409 `idempotency_conflict`, non facturé |
| T15 | Adresse inconnue | `address=Rue Inexistante 999, 1420 Braine-l'Alleud` | 404 `address_not_found`, `charged: false` |
| T15b | Adresse sans numéro de maison | `address=Avenue Alphonse Allard, 1420 Braine-l'Alleud` | 422 `address_imprecise`, non facturé |
| T16 | Section hors plan | `sections=basic,report` | 400 `invalid_request` (section inconnue, `details[0].field: sections`) ou 403 `section_not_allowed` avec `section` (connue mais hors plan ; pour tester le 403, demandez à Taxon de restreindre temporairement le plan de votre client de test ; votre plan est visible dans `GET /usage` sous `plan.sections_allowed`) |
| T17 | Plafond journalier test | 21e requête sur un même jour | 403 `quota_exceeded` avec `scope: "day"`, `limit: 20` (clé de test ; production : 300 par clé, 20 par utilisateur avec `scope: "user_day"` et `user_ref`), en-tête `Retry-After` (jusqu'à minuit) ; `billing.today.client_today` est monté à 20 auparavant (comptées : toutes les requêtes qui atteignent la recherche d'adresse, voir section 1 ; les rejeux par Idempotency-Key non) |
| T18 | Plafond par minute | 80 requêtes en 1 minute (à convenir avec Taxon ; consomme du quota de test) | 429 : de l'application `rate_limited` avec `Retry-After`, ou de nginx `{"error":"rate_limit_exceeded","retry_after":60}` sans en-tête ; pas de 5xx |
| T19 | Consommation JSON | `GET /usage?month=<this month>` | `environment: test`, `per_user` contient ag-0417 et ag-0022 avec `free_dedup` et `sections{package, avm}`, `sections_delivered{basic, parcel, avm, price_map}` (aussi `total.sections_delivered`), bloc `quality{error_pct, latency_p50_ms, latency_p95_ms, upstream_errors}`, `amount` 0 ; `?user_ref=AG-0417` filtre (insensible à la casse) ; `?maand=` = 400 avec `details[0].field: maand` ; bloc `plan {sections_allowed, max_per_minute, max_per_day, max_per_user_per_day, test_max_per_day, dedup_days, idempotency_hours, address_mode}` (votre plan sans prix) ; `pilot.expired` présent ; bloc `per_key[]` (2.4.1) avec votre clé de test (`environment: test`, `active: true`, `revoked_at: null`, sommes = `total`) |
| T20 | Consommation CSV | `format=csv` | `Content-Type: text/csv; charset=utf-8`, BOM, `;`, chaque champ entre guillemets, une ligne par requête de T06 à T14, 16 colonnes `request_id;timestamp;user_ref;case_ref;address;region;sections_requested;sections_delivered;charged;free_reason;dedup_of;price_package;price_avm;price_total;status_code;key_prefix` (`key_prefix` depuis la 2.4.1), nom de fichier `Content-Disposition` `taxon-usage-<client>-<month>.csv` |
| T21 | Liens photo et intégration | ouvrir une `photos[].url` de T06 avant et après `valid_until` ; récupérer une photo côté serveur dans l'heure et l'intégrer dans un rapport de test de ce dossier | d'abord 200 (image/jpeg, `Cache-Control: public, max-age=1800, immutable`), ensuite 404 `invalid_or_expired_token` ; URL avec token modifié = 404 ; la photo intégrée porte une mention de source visible (l'`attribution` de la réponse) à côté d'elle, fait au plus 640 px et n'apparaît que dans le rapport de ce dossier (2.5.0, section 5) |
| T22 | Caractéristiques et vignette | depuis T06, un élément de `basic.comparables[].items[]` | `features` avec les 22 clés (inconnu = `null`), `summary` une phrase non vide dans la langue demandée sans nom de portail, `photo_count` >= `len(photos)` ; si `thumbnail` n'est pas `null` : GET de `thumbnail.url` = 200 `image/jpeg` et la partie token de l'URL est identique à celle de `photos[0].url` (même photo, autre largeur) ; avec `type=land` (T24) `features` a exactement les 4 clés `plot_area_m2`, `zoning`, `flood_zone`, `land_type` |
| T23 | Délai dépassé et nouvelle tentative | forcer un délai client de 1 s, puis réessayer avec la même `Idempotency-Key` | la seconde tentative fournit la réponse sans double comptage |
| T24 | Terrain VL | `address=Buissestraat 17, 9690 Kluisbergen&type=land&sections=basic,avm` (sans `living_area_m2`) | 200, `sections_delivered: ["basic", "parcel", "price_map"]`, `avm: null`, message `{code: avm_not_available_for_land, section: avm}`, `billing.price.avm` = 0 et pas de `living_area_required` ; un bloc de points de référence avec `type: land`, `transaction: sale`, `radius_m: 5000` (mesuré 25 éléments), chaque élément `living_area_m2: null`, `plot_area_m2` et `price_per_m2_plot` remplis lorsque l'annonce porte une superficie, `features` avec les 4 clés (`land_type` `building_plot`, `project_land` ou `other`), `summary` commençant par « Building plot », « Development land » ou « Land », pas de terres agricoles, prairies, bois, terrains industriels ni emplacements de parking parmi les éléments ; `neighbourhood.land_price_level.count` > 0 avec `p25 <= median <= p75` (mesuré 116 annonces, médiane 160, après le correctif du 15-09-2026), `price_level: null`, `epc_prices: null`, message `housing_stats_not_available_for_land` ; `parcel` et `price_map` comme pour une maison |
| T24b | Alias terrain et validation | `type=grond` sur Rue de Fer 12, 5000 Namur (`Accept-Language: fr`), `type=terrain` sur Rue Royale Sainte-Marie 22, 1030 Schaerbeek, `type=bouwgrond` sur Rijselstraat 62, 8900 Ieper ; `type=kasteel` | 200 avec `comparables[0].type: "land"` et `type_label` dans la langue demandée (`terrain` pour fr, `grond` pour nl, `land` pour en), résumés dans cette langue (« Terrain à bâtir de 1085 m² à Namur, proposé depuis le 10-09-2026. ») ; `type=kasteel` = 400 `invalid_request` avec `details[0].issue` « type must be one of house, apartment, land » |
| T24c | Terrain, zone peu dense | `type=land` dans une commune rurale (par exemple Hauptstrasse 2, 4760 Büllingen) | `radius_m: 10000` lorsque moins de 10 annonces de terrains se trouvent dans les 5 km (mesuré : 25 dans les 10 km) ; avec moins de 10 dans les 10 km, `low_sample: true` sur le bloc et message `low_sample` (section basic) ; 0 annonce = message `no_comparables`, pack non facturé (`free_reason: no_result`) |
| T24d | Terrain, toutes années de publication (2.5.1) | `address=Buissestraat 17, 9690 Kluisbergen&type=land` | bloc `max_age_months: null` ; chaque élément porte `age_months` (entier >= 0, mois civils entiers depuis `published`) ; les éléments des 24 derniers mois (`age_months` < 24) viennent d'abord, triés par distance, puis les plus anciens, triés par distance (25 au plus par bloc : dans une zone dense comme Kluisbergen les 25 sont tous récents, mesuré 92 annonces récentes dans un rayon de 5 km ; les annonces plus anciennes apparaissent là où il y a moins de 25 récentes, par exemple Klosterstraße 12, 4700 Eupen : mesuré le 16-09-2026, 14 des 25 éléments avec `age_months` >= 24, réserve dans un rayon de 5 km de 11 récentes et 16 plus anciennes, `land_price_level.max_age_months` 60 avec `count` 11 ; ou Ooststraat 12, 8647 Lo-Reninge : 13 éléments dont 9 plus anciens, `max_age_months` 60 avec `count` 10) ; `neighbourhood.land_price_level.max_age_months` vaut 24, ou 60 lorsque moins de 10 annonces utilisables se trouvent dans les 24 mois ; avec 0 annonce, le message `no_comparables` dit "No land listings within 10 km (all publication years)." (dans la langue demandée) |

Clôture du test pilote : envoyez à Taxon la liste des `request_id` par scénario ; Taxon les vérifie dans le grand livre et confirme par écrit. La clé de production est ensuite délivrée (après signature de la convention).

## 12. Licence et règles d'utilisation

Résumé des règles contractuelles que l'API applique aussi techniquement ; la convention prime.

- **Par dossier.** Ne montrer les données qu'à l'expert et ne les reprendre que dans le rapport d'expertise du dossier pour lequel la requête a été faite ; chaque requête porte le véritable utilisateur (`X-User-Ref`) et le dossier (`X-Case-Ref`, contractuellement obligatoire, techniquement optionnel).
- **Conservation 30 jours maximum** pour les réponses brutes, journaux et caches ; seul le rapport finalisé (PDF) reste dans l'archive du dossier.
- **Pas de base de données dérivée** : ne pas agréger, indexer ou stocker les données (points de référence, statistiques, parcelles, géométrie de la carte des prix) de plusieurs requêtes dans votre propre base de données, couche cartographique, index ou modèle de prix ; **pas d'alimentation de modèle** (entraînement, calibrage, validation d'un algorithme ou système d'IA quelconque) ; **pas de revente ni d'extraction en masse** (pas de publication, d'export, de transmission à des tiers en dehors du rapport ; pas d'interrogation automatisée ou systématique en dehors d'un dossier).
- **Photos** (2.5.0) : récupérées pendant la validité du lien, utilisées uniquement pour le dossier, intégrées uniquement dans le rapport de ce dossier avec une mention de source visible à côté de chaque photo (sous réserve de la convention de partenariat, qui comprend la garantie du partenaire envers Taxon pour les réclamations de portails ou d'agents immobiliers) ; pas de republication, pas de conservation en dehors de ce rapport, pas de téléchargement en masse ; 640 px au plus.
- **Mention de la source et clause de non-responsabilité** visibles dans l'interface et dans chaque rapport (champs `attribution`, `disclaimer`) ; `source` par couche et `amenities.attribution` à côté de ces données ; marque blanche uniquement via un avenant distinct.
- **PEB tel qu'annoncé**, **pas de prix notariés** (ne jamais revendiquer de données de notaires, de VLABEL ou de transactions), **décision humaine** (aucune décision à effet juridique fondée exclusivement sur l'API ou l'AVM).
- **Clé secrète**, fuite signalée dans les 48 heures ; sur demande (au plus deux fois par an), un extrait des numéros de dossier en regard des requêtes. **La convention, les prix et les clés sont confidentiels ; cette documentation est publique.**

## 13. Liste de contrôle avant la mise en production

- [ ] Clé dans un coffre à secrets, accessible uniquement depuis le backend ; liste d'adresses IP autorisées communiquée à Taxon.
- [ ] `X-User-Ref` lié à votre véritable table d'utilisateurs (voir la définition en section 1) ; `X-Case-Ref` au dossier.
- [ ] Mention de la source et clause de non-responsabilité visibles à l'écran et dans le modèle de rapport (dans chaque langue que vous proposez).
- [ ] Lignes de source par couche (`source`, `attribution`) à côté des données.
- [ ] Tâche de suppression : réponses brutes et journaux contenant des données API de plus de 30 jours.
- [ ] Photos : récupérées pendant la validité, intégrées uniquement dans le rapport du dossier avec une mention de source visible par photo (2.5.0), pas de téléchargement en masse ; pas d'agrégation de points de référence ni de géométrie de la carte des prix entre dossiers.
- [ ] Monitoring sur `GET /health` et sur le pourcentage de 429/502/503 par jour.
- [ ] Vérification mensuelle de `GET /usage?format=csv` contre la facture Taxon.
- [ ] Gestion des clés dans le tableau de bord partenaire (section 15) : la personne qui affiche une clé une fois et renouvelle la clé live est connue ; votre client traite le message `key_rotation_pending` (le journaliser, changer de clé dans les 7 jours).
- [ ] Votre comptabilité lit `billing.price.package` et `billing.price.avm` (2.4.0) et les colonnes CSV `price_package`, `price_avm`, `price_total` ; le pack de base est une ligne par dossier, l'option AVM une seconde.

## 14. Changelog et correspondance 1.x vers 2.0

| Version | Date | Modifications |
|---|---|---|
| 2.5.1 | 16-09-2026 | Terrains sans limite d'âge. Avec `type=land`, les points de référence ne sont plus limités aux annonces publiées dans les 24 mois : toutes les annonces de terrains à vendre (mêmes exclusions de sous-type qu'avant) dans un rayon de 5 km (étendu une fois à 10 km s'il y en a moins de 10 au total) sont fournies, d'abord les annonces des 24 derniers mois (triées par distance) puis les plus anciennes (triées par distance), 25 au plus par bloc, pour que vous puissiez actualiser les prix demandés plus anciens selon l'évolution du marché dans le cadre du dossier (indexation des prix) ; les conditions de licence restent inchangées : pas de fusion, d'indexation ni de conservation entre dossiers. Nouveau champ d'élément `age_months` (mois civils entiers depuis `published`, uniquement avec `type: land`) ; le champ de bloc `max_age_months` vaut `null` pour un terrain (reste 24 pour maison et appartement). `neighbourhood.land_price_level` garde les 24 derniers mois et, avec moins de 10 annonces utilisables dans les 24 mois, s'élargit à 60 mois ; son `max_age_months` indique la fenêtre utilisée (24 ou 60). Le message `no_comparables` reçoit un texte propre aux terrains sans "(< 24 mois)". Maison et appartement inchangés. Additif : pas de changement de chemins ni de champs existants. Version du service 2.5.1. Correctif du 16-09-2026 (même version 2.5.1) : `days_online` est documenté comme nullable (`null` lorsque la source n'a pas enregistré de durée en ligne, surtout des annonces plus anciennes hors ligne ; `last_seen` vaut alors `published`) ; la finalité des annonces plus anciennes est formulée comme une indexation des prix dans le cadre du dossier (licence inchangée) ; le scénario de test T24d utilise une adresse d'exemple qui renvoie effectivement des annonces plus anciennes ; les réponses terrain conservées sous 2.5.0 ne sont plus rejouées depuis le cache d'entrée. |
| 2.5.0 | 15-09-2026 | Terrains et règle photos. Nouvelle valeur `type=land` (alias `grond`, `terrain`, `bouwgrond`) pour les terrains à bâtir : un bloc de points de référence avec des annonces de terrains à vendre dans un rayon de 5 km (étendu une fois à 10 km s'il y en a moins de 10 ; champ de bloc `low_sample` et message `low_sample`), éléments avec `plot_area_m2` et `price_per_m2_plot` (champs de logement `null`, `features` limité à `plot_area_m2`, `zoning`, `flood_zone`), `neighbourhood.land_price_level` {radius_m, count, price_per_m2_plot {p25, median, p75}, price_kind, max_age_months} tandis que `price_level` et `epc_prices` valent `null` (message `housing_stats_not_available_for_land`) ; pas d'AVM pour un terrain (message `avm_not_available_for_land`, section non fournie, non facturée) ; `living_area_m2` facultatif ; `parcel` et `price_map` inchangés ; prix du pack de base. Règle photos assouplie : les photos peuvent être intégrées dans le rapport du dossier avec une mention de source visible par photo, sous réserve de la convention de partenariat (pas de republication, pas de conservation en dehors de ce rapport, pas de téléchargement en masse, 640 px au plus). Correctif du 15-09-2026 (même version) : les points de référence terrain et `land_price_level` sont limités aux terrains à bâtir, terrains à projet et terrains sans sous-type plus précis (terres agricoles, prairies, bois, terrains industriels et emplacements de parking exclus par sous-type) ; `land_price_level` ne compte que les annonces entre 15 et 5 000 EUR par m² de terrain ; nouvelle clé `LandFeatures` `land_type` (`building_plot` | `project_land` | `other`), le premier mot de `summary` la suit (additif). Additif : pas de changement de chemins ni de champs existants. Version du service 2.5.0. |
| 2.4.2 | 14-09-2026 | Plan parcellaire cadastral actuel. La section `parcel` lit désormais la couche PlanParcellaire du SPF Finances (situation fiscale courante, aujourd'hui 01-01-2027) au lieu de l'extrait annuel INSPIRE (01-01-2026) ; la couche INSPIRE reste la solution de repli en cas de panne ou de réponse vide. Nouveau champ `fiscal_situation` (date ISO de la situation fiscale du plan parcellaire, p. ex. `2027-01-01` ; `2026-01-01` si le repli INSPIRE a répondu ; `null` si inconnu) sur `parcel`, sur chaque élément de `parcels[]` et sur chaque élément de `parcel_candidates[]`. `source` mentionne la couche (`CadGIS PlanParcellaire (SPF Finances)` ou `CadGIS INSPIRE (SPF Finances)`). Additif : aucun changement aux chemins, paramètres ou champs existants. Version du service 2.4.2. |
| 2.4.1 | 08-09-2026 | Consommation par clé API : `/usage` reçoit le bloc `per_key[]` (par clé : `prefix`, `label`, `environment`, `active`, `revoked_at`, `requests`, `cases`, `charged`, `sections`, `sections_delivered`, `amount`, `last_activity` ; mêmes filtres que le reste de la réponse ; les clés sans requête dans le mois ne sont pas listées, les clés révoquées avec des requêtes le sont). CSV : nouvelle dernière colonne `key_prefix` (16 colonnes). Aucun changement aux chemins, paramètres ou champs existants. Version du service 2.4.1 |
| 2.4.0 | 08-09-2026 | Pack de base : `basic` + `parcel` + `price_map` forment désormais un seul pack par dossier ; l'AVM reste une option distincte. `billing.price` devient `{package, avm, total, currency, excl_vat}` ; dans `/usage`, `total.sections`, `per_user[].sections` et `per_section` utilisent `package`/`avm`, `total.sections_delivered` ajouté ; colonnes CSV `price_package`, `price_avm`, `price_total`. Demander `parcel` ou `price_map` séparément est normalisé vers le pack. Version du service 2.4.0 |
| 2.3.0 | 08-09-2026 | Gestion des clés par le partenaire (section 15) : clés affichées exactement une fois dans le tableau de bord partenaire sur taxon.be (dans les 7 jours, puis la copie chiffrée est détruite) ; clés de test propres (au plus 3 actives, révocables soi-même) ; rotation de la clé live avec un chevauchement de 7 jours pendant lequel les réponses obtenues avec l'ancienne clé portent le message `key_rotation_pending` (aussi dans `/usage`), ensuite `403 key_revoked` ; la première clé live reste délivrée par Taxon après signature ; aucun changement de chemin, de paramètre ni de champ ; version du service 2.3.0 |
| 2.2.2 | 07-09-2026 | Corrections après le premier test d'intégration externe (Propteo) : les liens photo livrent toujours une vignette serveur (640 px par défaut, jamais l'original) ; `rental_value` toujours dans `rental_value_range` ; `parcel_candidates`, `parcel_candidates_source` et `parcel_candidates_remark` toujours présents (`null` avec `capakeys`) ; `parcel_candidates[].direction` et `garden_orientation` indépendants de la langue (N/E/S/W) ; `zoning.category` est une énumération anglaise ; lignes `source` traduites pour fr/en ; une seule règle de nom de commune pour tout le paquet (pas d'exonymes) ; `new_build` seulement si `year_built` ne le contredit pas ; résumés français accordés en genre ; erreurs d'en-tête et de paramètre dans un seul 400 ; `free_reason: dedup` aussi avec une clé de test quand chaque section fournie l'avait déjà été ; `pilot_status.expired` partout ; bloc `plan` dans `/usage` ; `Content-Type` CSV avec un seul charset ; `Cache-Control` et `X-Content-Type-Options` une seule fois. Version du service 2.2.2. |
| 2.2.1 | 07-09-2026 | Ronde de QA adversariale (aucun changement de clé ni de chemin) : numéros de boîte `box 3`, `b3`, `app 3` reconnus ; une adresse sans code postal ni commune donne `422 address_imprecise` ; `epc=A+` avec un plus brut donne 400 (envoyez `A%2B`) ; le cache d'entrée et la comparaison `Idempotency-Key` couvrent tous les paramètres et la langue (autre entrée = nouveau bundle, toujours gratuit pour les sections déjà fournies ; même clé avec d'autres paramètres = 409) ; `section_missing` pour un sous-bloc de basic porte `section: basic` avec le nom du bloc dans le message ; `/usage` refuse un paramètre envoyé deux fois ; géométries price_map toujours valides ; requêtes identiques simultanées en pilote/test comptées une fois. Version du service 2.2.1. |
| 2.2.0 | 07-09-2026 | Chaque point de référence porte `features` (caractéristiques structurées de l'annonce, inconnu = `null`), `summary` (une phrase, nl/fr/en), `thumbnail {url, valid_until}` (photo principale 160 px, même photo que `photos[0]`) et `photo_count` ; pas de texte d'annonce, pas de changement de prix ; version de service 2.2.0 |
| 2.0.0 | 07-09-2026 | Contrat anglais : chemins `/address`, `/usage` ; en-tête `X-User-Ref` (alias `X-Gebruiker-Ref`, `X-Kantoor-Ref` obsolètes), `X-Case-Ref` (alias `X-Dossier-Ref`), `Accept-Language: en` ; tous les paramètres, clés, valeurs d'énumération et codes de message en anglais ; corps d'erreur avec `charged`, `details[] {field, issue}`, scope `user_day` ; nouvelle section `price_map` (service 2.1.0 : messages `price_map_sparse`, `price_map_unavailable`, `upstream_errors.price_map: no_coverage`, une clé de prix et une colonne CSV pour la carte des prix, remplacées par les clés du pack en 2.4.0) ; `condition` atteint désormais l'AVM ; `/health` conserve ses clés, `versie` affiche la version du service (2.1.0 à ce moment-là) |
| 1.2.0 | 07-09-2026 | Plusieurs parcelles par bien (`capakeys`, candidates, totaux), repli de superficie du terrain, filtre chambres, nouvelles erreurs 422 et messages |
| 1.1.0 | 04-09-2026 | « agence » est devenu « utilisateur » : en-tête utilisateur obligatoire, en-tête agence alias obsolète ; plafonds 20/300/60 fixés ; le chien de garde suspend l'utilisateur |
| 1.0.0 | 03-09-2026 | Premier contrat : paquet adresse, consommation, health ; dédoublonnage 30 jours ; Idempotency-Key 24 h ; tour QA |

Correspondance (ancien nom 1.x -> nom 2.0). Les anciens noms ne fonctionnent plus, à l'exception des trois alias d'en-tête.

| Où | 1.x | 2.0 |
|---|---|---|
| Chemins | `/adres`, `/verbruik` | `/address`, `/usage` |
| En-têtes | `X-Gebruiker-Ref` (1.1), `X-Kantoor-Ref` (1.0), `X-Dossier-Ref` | `X-User-Ref`, `X-Case-Ref` (les anciens noms restent des alias) |
| Query /address | `adres`, `secties=basis`, `type=huis\|appartement`, `opp`, `bouwjaar`, `staat=nieuw\|zeer_goed\|goed\|matig\|te_renoveren`, `slaapkamers`, `opp_grond` | `address`, `sections=basic`, `type=house\|apartment\|land` (terrain 2.5.0), `living_area_m2`, `year_built`, `condition=excellent\|very_good\|good\|average\|poor`, `bedrooms`, `plot_area_m2` |
| Query /usage | `maand`, `formaat`, `gebruiker_ref` | `month`, `format`, `user_ref` |
| Niveau supérieur | `omgeving`, `uit_cache`, `adres`, `secties_geleverd`, `basis`, `facturatie`, `meldingen`, `fouten_upstream`, `bronvermelding`, `gegenereerd_op` | `environment`, `from_cache`, `address`, `sections_delivered`, `basic`, `billing`, `notices`, `upstream_errors`, `attribution`, `generated_at` |
| address | `invoer`, `genormaliseerd`, `bus`, `postcode`, `gemeente`, `gewest`, `niscode`, `geocode_bron`, `precisie=huisnummer` | `input`, `normalized`, `box`, `postal_code`, `municipality`, `region`, `nis_code`, `geocoder`, `precision=house_number` |
| bloc comparables | `transactie=koop\|huur`, `adres_modus=huisnummer\|straat`, `straal_m`, `aantal`, `max_leeftijd_maanden`, `uitgesloten_eigen_pand`, `slaapkamers_filter {gevraagd, bereik, toegepast, aantal_binnen_filter}` | `transaction=sale\|rent`, `address_mode=house_number\|street`, `radius_m`, `count`, `max_age_months`, `excluded_subject_property`, `bedrooms_filter {requested, range, applied, count_within_filter}` ; nouveau en 2.5.0 (sans équivalent 1.x) : `low_sample` (terrain) |
| élément comparable | `adres`, `afstand_m`, `prijs`, `prijs_soort=vraagprijs`, `prijs_per_m2`, `opp_wonen_m2`, `opp_grond_m2`, `slaapkamers`, `epc_kengetal_kwh_m2`, `epc_bron`, `bouwjaar`, `staat`, `bebouwing=open\|halfopen\|gesloten\|appartement`, `nieuwbouw`, `publicatie`, `dagen_online`, `laatst_gezien`, `bron=advertentie`, `historiek[] {datum, prijs, gebeurtenis=publicatie\|prijsdaling\|prijsstijging\|herpublicatie\|offline}`, `fotos[] {geldig_tot}` | `address`, `distance_m`, `price`, `price_kind=asking_price`, `price_per_m2` (per m² of living area; per month for `rent`, unlike `price_level.rent_per_m2_year`), `living_area_m2`, `plot_area_m2`, `bedrooms`, `epc_kwh_m2`, `epc_source`, `year_built`, `condition`, `building_type=detached\|semi_detached\|terraced\|apartment`, `new_build` (advertised as new build and `year_built`, when known, at most 3 years back; otherwise `false`), `published`, `days_online` (`null` when the source recorded no online duration, mostly older offline listings; `last_seen` then equals `published`), `last_seen`, `source=listing`, `history[] {date, price, event=published\|price_drop\|price_increase\|republished\|offline}`, `photos[] {valid_until}` ; nouveau en 2.2.0 (sans équivalent 1.x) : `features {...}`, `summary`, `thumbnail {url, valid_until}`, `photo_count` ; nouveau en 2.5.0 : `price_per_m2_plot` (terrain) |
| neighbourhood (anciennement `buurtstats`) | `sector {naam, niveau, gemeente}`, `gebouwenpark {niveau, peildatum, totaal, verdeling[] {cat, aantal}, bron}`, `prijsniveau {koop_m2, huur_m2_jaar, brutorendement_pct, brutorendement_p25_p75, straal_m, prijs_soort}`, `mediaan`, `epc_prijzen {labels {mediaan_m2, aantal}}`, `veiligheid {niveau, gemeente, jaar, woninginbraak_per_1000, misdrijven_per_1000, gewest_*, jaren[], bron}` | `sector {name, level, municipality}`, `building_stock {level, reference_date, total, distribution[] {category, count}, source}`, `price_level {sale_per_m2, rent_per_m2_year, gross_yield_pct, gross_yield_p25_p75, radius_m, price_kind}`, `median`, `epc_prices {labels {median_per_m2, count}}`, `safety {level, municipality, year, burglaries_per_1000, crimes_per_1000, region_*, years[], source}` ; nouveau en 2.5.0 : `land_price_level {radius_m, count, price_per_m2_plot {p25, median, p75}, price_kind, max_age_months}` (terrain) |
| amenities (anciennement `voorzieningen`) | `schaal`, `straal_m`, `aantal_pois`, `deelscores {openbaar_vervoer, zorg, winkels, sport_cultuur, onderwijs, groen} {aantal}`, `top_10[] {naam, afstand_m}`, `attributie` | `scale`, `radius_m`, `poi_count`, `sub_scores {public_transport, healthcare, shops, sport_culture, education, green_space} {count}`, `top_10[] {name, distance_m}`, `attribution` |
| parcel | `gewest`, `oppervlakte_m2`, `oppervlakte_kadastraal_m2`, `perceel_breedte_m`, `perceel_diepte_m`, `gevel_breedte_m`, `bebouwde_opp_m2`, `gebouwen_aantal`, `orientatie_tuin(_graden)`, `bestemming {categorie}`, `voorkooprecht {status=ja\|geen\|onbekend, bron, dekking_pct, percelen, percelen_onbekend}`, `opmerking`, `bron`, `hoofdperceel`, `percelen[] {hoofdperceel, afstand_tot_adres_m}`, `percelen_aantal`, `oppervlakte_totaal_m2`, `oppervlakte_kadastraal_totaal_m2`, `bestemming_gezamenlijk`, `voorkooprecht_gezamenlijk`, `percelen_niet_gevonden`, `opp_grond_bron`, `perceel_kandidaten[] {oppervlakte_m2, richting, bebouwd, afstand_m}`, `perceel_kandidaten_bron`, `perceel_kandidaten_opmerking` | `region`, `area_m2`, `cadastral_area_m2`, `fiscal_situation` (2.4.2; aussi dans `parcels[]` et `parcel_candidates[]`, sans équivalent 1.x), `width_m`, `depth_m`, `frontage_m`, `built_area_m2`, `buildings_count`, `garden_orientation(_deg)` (N/NE/E/... language-independent; `null` for apartments and for houses without a garden part), `zoning {category}` (category = English enum, label = official regional text), `preemption_right {status=yes\|none\|unknown, source, coverage_pct, parcels, parcels_unknown}`, `remark`, `source`, `main_parcel`, `parcels[] {is_main, distance_to_address_m}`, `parcels_count`, `total_area_m2`, `cadastral_total_area_m2`, `zoning_combined`, `preemption_right_combined`, `parcels_not_found`, `plot_area_source=cadastre_main_parcel\|cadastre_<n>_parcels` (n = 2 to 10; `partner` only in `avm.inputs_used`), `parcel_candidates[] {area_m2, direction=N\|E\|S\|W, built, distance_m}`, `parcel_candidates_source`, `parcel_candidates_remark` |
| avm | `waarde`, `vork`, `vork_90`, `huurwaarde`, `huurwaarde_vork`, `betrouwbaarheid`, `n_comparables(_500m, _1km)`, `invoer_gebruikt {opp_wonen_m2, bouwjaar, staat, slaapkamers, opp_grond_m2, opp_grond_bron}`, `prijs_soort=vraagprijs` | `value`, `range`, `range_90`, `rental_value` (always inside `rental_value_range`), `rental_value_range`, `confidence {score, label, fsd_pct}` (label follows fsd_pct: high up to 14, medium up to 24, low above), `comparables_count(_500m, _1km)`, `inputs_used {living_area_m2, year_built, condition, bedrooms, plot_area_m2, plot_area_source}`, `price_basis=asking_price_model`, `model` (model and calibration version: currently v3.1 for Flanders, regionally calibrated; v3.6 for Wallonia and Brussels, indicative) |
| billing (anciennement `facturatie`) | `gebruiker_ref`, `dossier_ref`, `aangerekend`, `reden_gratis=geen_resultaat\|fout\|pilot_uitgeput`, `prijs {basis, totaal, munt, excl_btw}`, `dedup_van`, `dedup_geldig_tot`, `maand`, `verbruik_maand_tot_nu {dossiers, bedrag}`, `pilot_stand {actief, gebruikt, resterend, einde, verlopen}`, `dag {klant_vandaag, gebruiker_vandaag}` | `user_ref`, `case_ref`, `charged`, `free_reason=no_result\|error\|pilot_exhausted`, `price {package, avm, total, currency, excl_vat}` (2.4.0), `dedup_of`, `dedup_valid_until`, `month`, `month_to_date {cases, amount}`, `pilot_status {active, used, remaining, ends, expired}`, `today {client_today, user_today}` |
| notices (anciennement `meldingen`) | `{code, sectie, bericht}`; `opp_required`, `avm_indicatief`, `geen_comparables`, `adres_straatniveau`, `geocoder_deels_onbereikbaar`, `gewest_conflict`, `eigen_pand_uitgesloten`, `hoofdperceel_niet_in_capakeys`, `perceel_analyse_onvolledig`, `opp_grond_niet_kadastraal_bevestigd`, `slaapkamers_filter_losgelaten`, `sectie_ontbreekt` (`upstream_fout`), `test_omgeving`, `pilot_uitgeput` | `{code, section, message}`; `living_area_required`, `avm_indicative_not_regionally_calibrated`, `no_comparables`, `address_street_level`, `geocoder_partially_unavailable`, `region_conflict`, `subject_property_excluded`, `main_parcel_not_in_capakeys`, `parcel_analysis_incomplete`, `plot_area_not_cadastral`, `bedrooms_filter_dropped`, `section_missing` (`upstream_error`), `test_environment`, `pilot_exhausted` ; nouveau en 2.3.0 (sans équivalent 1.x) : `key_rotation_pending` ; nouveau en 2.5.0 : `avm_not_available_for_land`, `low_sample`, `housing_stats_not_available_for_land` |
| clés upstream_errors | `basis`, `basis.comparables`, `basis.buurtstats`, `basis.voorzieningen`, `buurtstats.gebouwenpark`, `buurtstats.veiligheid`, `buurtstats.prijsniveau`, `buurtstats.epc_prijzen`; valeur `upstream_fout` | `basic`, `basic.comparables`, `basic.neighbourhood`, `basic.amenities`, `neighbourhood.building_stock`, `neighbourhood.safety`, `neighbourhood.price_level`, `neighbourhood.epc_prices`; valeur `upstream_error` |
| corps d'erreur | `aangerekend`, `details[] {veld, fout}`, `sectie`, `gebruiker_ref`, `reden`, `sinds`, `limiet`, `scope=gebruiker_day`; `gebruiker_ref_required`, `gebruiker_ref_conflict` | `charged`, `details[] {field, issue}`, `section`, `user_ref`, `reason`, `since`, `limit`, `scope=user_day`; `user_ref_required`, `user_ref_conflict` |
| /usage | `klant`, `maand`, `omgeving`, `gebruiker_ref`, `totaal {opvragingen, dossiers, aangerekend, gratis_dedup, gratis_pilot, gratis_storing, secties, bedrag, munt, excl_btw, duur_ms_gem}`, `per_gebruiker[] {secties_geleverd, laatste_activiteit}`, `per_dag[] {dag}`, `per_sectie {geleverd, aangerekend, bedrag}`, `kwaliteit {fout_pct, latentie_p50_ms, latentie_p95_ms, upstream_fouten}`, `gegenereerd_op` | `client`, `month`, `environment`, `user_ref`, `total {requests, cases, charged, free_dedup, free_pilot, free_error, sections, amount, currency, excl_vat, duration_ms_avg}`, `per_user[] {sections_delivered, last_activity}`, `per_key[] {prefix, label, environment, active, revoked_at, requests, cases, charged, sections, sections_delivered, amount, last_activity}` (2.4.1, nom interne `per_sleutel`, sans équivalent 1.x), `per_day[] {day}`, `per_section {delivered, charged, amount}` (2.4.0 : clés `package`, `avm` ; `total.sections {package, avm}`, `total.sections_delivered`), `quality {error_pct, latency_p50_ms, latency_p95_ms, upstream_errors}`, `generated_at` |
| en-tête CSV | `request_id;tijdstip;gebruiker;dossier_ref;adres;gewest;secties_gevraagd;secties_geleverd;aangerekend;reden_gratis;dedup_van;prijs_basis;prijs_parcel;prijs_avm;prijs_totaal;status_code` | `request_id;timestamp;user_ref;case_ref;address;region;sections_requested;sections_delivered;charged;free_reason;dedup_of;price_package;price_avm;price_total;status_code;key_prefix` (2.4.0 ; `key_prefix` 2.4.1, nom interne `sleutel_prefix`) ; fichier `taxon-usage-<client>-<month>.csv` |
## 15. Gérer vos clés

Depuis le 08-09-2026, vous gérez vos clés vous-même dans le tableau de bord partenaire sur taxon.be (https://taxon.be/api_partner, section « Gestion »). Chaque action y est journalisée (qui, quand, depuis quelle adresse IP) et signalée à Taxon ; la valeur d'une clé n'apparaît jamais dans un journal ni dans un e-mail.

| Action | Fonctionnement |
|---|---|
| Afficher une clé une seule fois | Une nouvelle clé n'est pas envoyée par e-mail. Le tableau de bord l'affiche exactement une fois (« Afficher la clé une seule fois »), dans les 7 jours suivant sa création ; copiez-la aussitôt dans votre coffre à secrets. Jusqu'à ce moment, Taxon n'en conserve qu'une copie chiffrée : après le premier affichage, ou après 7 jours, la copie est détruite et la clé ne peut plus être affichée. Les clés délivrées avant le 08-09-2026 ne peuvent pas être affichées ; demandez-en une nouvelle ou créez-en une vous-même. |
| Clés de test | Créez-les vous-même (« Créer une clé de test », avec un libellé), au plus 3 clés de test actives ; révoquez-les vous-même (« Révoquer » : la clé cesse immédiatement de fonctionner, `401 invalid_api_key`). |
| Première clé live | Délivrée par Taxon après la signature de la convention, prête à être affichée une fois dans votre tableau de bord. Vous ne pouvez pas créer vous-même une première clé live (`live_key_not_allowed`) ; le formulaire « Demander une clé live » du tableau de bord envoie un e-mail à Taxon. |
| Renouveler une clé live | « Renouveler la clé live » crée une nouvelle clé live (affichez-la une fois, mettez-la en production). La clé live précédente continue de fonctionner pendant 7 jours ; durant cette période, chaque réponse de `/address` et de `/usage` obtenue avec l'ancienne clé porte le message `key_rotation_pending` (`section: null`, message avec la date de fin). Après 7 jours, l'ancienne clé est refusée (`403 key_revoked`, ensuite `401 invalid_api_key`). Une clé live n'est jamais révoquée depuis le tableau de bord : renouvelez-la, ou demandez à Taxon de la révoquer immédiatement. |
| Fuite ou soupçon d'abus | Renouvelez (live) ou révoquez (test) immédiatement et prévenez info@taxon.be si l'ancienne clé doit cesser sur-le-champ plutôt qu'après 7 jours. |

Codes d'erreur des actions du tableau de bord (affichés en texte dans le tableau de bord, jamais sur `/address` ni `/usage`) : `key_already_revealed`, `reveal_expired`, `not_revealable`, `key_limit_reached`, `live_key_not_allowed`, `not_self_revocable`.

Procédure de rotation (recommandée) : 1. renouvelez dans le tableau de bord et affichez la nouvelle clé une fois ; 2. placez la nouvelle clé dans votre coffre à secrets et déployez ; 3. vérifiez que vos réponses ne portent plus `key_rotation_pending` ; 4. l'ancienne clé expire d'elle-même après 7 jours.
