# Taxon Partner API 2.0: integratiegids (NL)

Versie 2.5.1, 16-09-2026 (Engels contract; zie sectie 14 voor de mapping vanaf de Nederlandse 1.x-namen; dezelfde tabel is sectie 12 van de HTML-documentatie). Dit is de Nederlandse vertaling van integration-guide.md (EN); de Franse versie is guide-fr.md. Volledige documentatie: https://taxonapi.be/partner-docs/ (EN), /partner-docs/index-fr (FR), /partner-docs/index-nl (NL); machineleesbaar: /partner-docs/openapi.yaml.
Voor het technisch personeel van de partner. Geen prijzen in dit document; die staan in de overeenkomst. De overeenkomst, de prijzen en uw sleutels zijn vertrouwelijk; deze documentatie zelf is openbaar gepubliceerd.

## 1. Quickstart in vijf regels

1. Basis-URL `https://taxonapi.be/api/v1/partner/`, enkel HTTPS, server-naar-server.
2. Elke aanvraag: `X-Api-Key` (tx_live_ of tx_test_) en `X-User-Ref` (verplicht, uw gebruiker-id). Aanbevolen: `Idempotency-Key` (UUID per aanvraag), `Accept-Language: en` (of `fr`, `nl`). `X-Case-Ref` (uw dossiernummer) is technisch optioneel (een aanvraag zonder wordt aanvaard; `billing.case_ref` is dan `null` en de CSV-kolom leeg) maar contractueel verplicht: elke aanvraag hoort bij een expertisedossier, stuur hem dus altijd mee.
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` (standaard) is het basispakket: `basic` + `parcel` + `price_map` samen, één prijs per dossier; `avm` is een aparte optie (`parcel` of `price_map` apart opvragen wordt genormaliseerd naar het pakket). `living_area_m2` is verplicht voor `avm` (niet bij `type=land`: geen AVM voor grond, zie de deelsectie Gronden in sectie 2). Bestaat een pand uit meerdere percelen (tuin, garage, weide), geef ze dan mee als `capakeys` (maximaal 10): eerst opvragen zonder om de kandidaten te krijgen, dan met (sectie 3).
4. Zelfde gebruiker + zelfde pand binnen 30 dagen = gratis (`from_cache: true` of meldingen `dedup` per sectie); de AVM-optie later toegevoegd = enkel die optie; een andere gebruiker = een nieuwe aanrekening. Zelfde `Idempotency-Key` binnen 24 u = zelfde antwoord (`X-Idempotent-Replay: true`), niet geteld. AVM zonder `living_area_m2` is geen fout: HTTP 200 met melding `living_area_required`, sectie overgeslagen.
5. Toon altijd `attribution` en `disclaimer` uit het antwoord, letterlijk en zichtbaar; bewaar ruwe antwoorden maximaal 30 dagen; foto's: ophalen binnen de geldigheid van de link en enkel inbedden in het rapport van het dossier, met een zichtbare bronvermelding (sectie 5).

**Gebruiker.** Een gebruiker is de eenheid die u zelf bij elke aanvraag meegeeft: een kantoor, een medewerker, een filiaal of een dossierbehandelaar. Die keuze bepaalt de aanrekening: dezelfde gebruiker die hetzelfde pand binnen 30 dagen opnieuw opvraagt, betaalt niet opnieuw (dedup); een andere gebruiker wel. Per gebruiker geldt een dagplafond, per sleutel een globaal plafond. Plafonds: 20 aanvragen per gebruiker per dag, 300 per sleutel per dag, 60 per minuut met een burst van 20 (testsleutel: 20 per dag). Wat meetelt in de dagplafonds (en als aanvraag in `/usage`): elke aanvraag die de adresopzoeking bereikt, dus 200 (vers, dedup of gedeeltelijk), 404 `address_not_found`, 422 `address_imprecise`, `capakey_too_far` en `type_unsupported`, 502 en 504; niet meegeteld: 400, 401, 403, 405, 409, 422 `capakey_invalid`, 429, 503 en Idempotency-Key-replays.

**Dossier.** `X-Case-Ref` is uw dossiernummer (`A-Z a-z 0-9 . _ : @ / space -`, maximaal 64). Het komt terug in `billing.case_ref` en in de gebruiks-CSV, zodat elke aanvraag in uw boekhouding aan een dossier gekoppeld kan worden. Elke aanvraag hoort bij een concreet expertisedossier (contractuele regel).

## 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
```

Zet de sleutel nooit op de commandolijn in gedeelde omgevingen; gebruik een omgevingsvariabele of een secret store.

Wat de eerste oproep teruggeeft (live, 07-09-2026, testsleutel, ingekort): `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` met `range`, `range_90`, `rental_value_range` en `confidence.label: "medium"`, `avm.price_basis: "asking_price_model"`, melding `avm_indicative_not_regionally_calibrated` (Wallonië), `attribution: "References: Taxon (taxon.be)"`. De voorbeelden gebruiken `uuidgen` en `jq` (Linux/macOS); in Windows PowerShell gebruikt u `[guid]::NewGuid()` voor de sleutel en laat u het `| jq`-filter weg (met een lege waarde stuurt curl helemaal geen `Idempotency-Key`-header).

**Gebruik per API-sleutel (2.4.1).** `GET /usage` draagt ook het blok `per_key` (tussen `per_user` en `per_day`): het gebruik per API-sleutel, met dezelfde filters als de rest van het antwoord (maand, de omgeving van de aanroepende sleutel, `user_ref`). Per sleutel: `prefix` (de eerste 12 tekens, zoals in uw sleutellijst), `label` (`null` als leeg), `environment`, `active`, `revoked_at` (`null` zolang actief), `requests`, `cases`, `charged`, `sections`, `sections_delivered`, `amount` en `last_activity`; gesorteerd op `amount`, dan `requests`, aflopend. Sleutels zonder aanvragen in de maand staan er niet in; een ingetrokken sleutel met aanvragen in die maand wel (`active: false`, `revoked_at` ingevuld). De sommen over `per_key` zijn gelijk aan `total`. De CSV (`format=csv`) krijgt `key_prefix` als 16e en laatste kolom. Voorbeeld met een huidige live-sleutel en een vorige live-sleutel die intussen ingetrokken is:

```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"
  }
]
```

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

`type=land` (aliassen `grond`, `terrain`, `bouwgrond`) vraagt de bundel voor een bouwgrond. Tegenover een huis of appartement: één referentieblok (`type: land`, `transaction: sale`, geen huurblok) met grondzoekertjes te koop (bouwgronden, projectgronden en gronden zonder nader subtype; landbouwgrond, weiden, bos, boomgaarden, industrie- en KMO-grond, staanplaatsen, garages en recreatiegrond worden op basis van het geadverteerde subtype uitgesloten), van alle publicatiejaren (2.5.1: geen leeftijdsgrens meer, zodat u de vraagprijzen van oudere zoekertjes binnen het dossier kunt aanpassen aan de marktevolutie (prijsindexering); de licentievoorwaarden blijven gelden: niet samenvoegen, indexeren of opslaan over dossiers heen), binnen 5 km van het adres (eenmaal uitgebreid tot 10 km bij minder dan 10 in totaal; nog altijd minder dan 10 = blokveld `low_sample: true` en melding `low_sample`), gerangschikt met eerst de zoekertjes van de laatste 24 maanden (op afstand) en daarna de oudere (op afstand), maximaal 25 per blok; elk item draagt `age_months` (hele kalendermaanden tussen `published` en vandaag, 0 voor deze maand) en het blokveld `max_age_months` is `null` voor grond; elk item draagt `plot_area_m2` en `price_per_m2_plot` (vraagprijs per m² grond), de woningvelden (`living_area_m2`, `price_per_m2`, `bedrooms`, `epc_label`, `year_built`, `condition`, `building_type`, `new_build`) zijn `null`, `features` is beperkt tot `{plot_area_m2, zoning, flood_zone, land_type}` (`zoning` momenteel altijd `null`; de bestemming van het perceel van het pand staat in `parcel.zoning`; `land_type` is `building_plot`, `project_land` of `other` en bepaalt het eerste woord van `summary`: "Bouwgrond", "Projectgrond" of "Grond") en `summary` luidt bijvoorbeeld "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}` komt uit dezelfde grondzoekertjes (zelfde uitsluitingen) van de laatste 24 maanden (een prijsniveau moet actueel zijn; 2.5.1: bij minder dan 10 bruikbare zoekertjes binnen 24 maanden wordt het venster verruimd tot 60 maanden en zegt `max_age_months` welk venster gebruikt is, 24 of 60); enkel zoekertjes met een grondoppervlakte en een vraagprijs tussen 15 en 5.000 EUR per m² grond tellen mee in de kwartielen (`count`), zodat landbouwgrond die als bouwbaar geadverteerd staat en plaatshouder-oppervlakten het niveau niet vertekenen (items buiten die band blijven in de lijst met hun eigen `price_per_m2_plot`); `price_level` en `epc_prices` zijn `null` (melding `housing_stats_not_available_for_land`). **Geen AVM voor grond**: `sections=basic,avm` blijft HTTP 200 met `avm: null`, melding `{code: avm_not_available_for_land, section: avm}` en geen aanrekening van de optie; `living_area_m2` mag ontbreken. `parcel` (voor grond de kern: bestemming, voorkooprecht, meerdere percelen via `capakeys`) en `price_map` (de woningkaart, geen grondprijskaart) zijn ongewijzigd; de facturatie is het basispakket zoals gewoonlijk.

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

Antwoord (echt, 15-09-2026, testsleutel, aanvraag `f0c372798b3f285b66ab4b584819be57`; de 2.5.1-velden `age_months` en `max_age_months: null` toegevoegd), ingekort tot één referentiepunt met één foto en de hoofdvelden van het perceel; `building_stock`, `safety`, `amenities`, `parcels`, `parcel_candidates` en `price_map` vervangen door `"..."`:

```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. Meerdere percelen per pand: eerst kandidaten, dan capakeys

**Bron van de perceelgegevens (2.4.2).** De sectie parcel leest het actuele kadastraal percelenplan van de FOD Financiën (CadGIS PlanParcellaire, lopende fiscale toestand, vandaag 01-01-2027); de jaarlijkse INSPIRE-momentopname (01-01-2026) is enkel nog de terugval bij een storing of een leeg antwoord. `fiscal_situation` (op `parcel`, op elk item van `parcels[]` en op elk item van `parcel_candidates[]`) geeft de ISO-datum van de fiscale toestand waaruit het perceel komt; `source` vermeldt de laag. De datum is de fiscale toestand sinds dewelke de huidige versie van dit perceel geldt: een perceel dat al jaren ongewijzigd is, draagt een oudere datum (bv. 2019-01-01), terwijl het plan waaruit het gelezen wordt altijd de lopende fiscale toestand is. Een oudere datum betekent dus geen verouderde data; enkel `2026-01-01` samen met een INSPIRE-`source` wijst op de jaarlijkse momentopname als terugval. Een perceel dat na de laatste momentopname gesplitst of samengevoegd werd (bv. 45043C0460/00R000 in Kluisbergen: één perceel van 921 m² in de momentopname, 460R 392 m² + 460X 529 m² in het actuele plan) komt in zijn huidige vorm binnen.

Een rijwoning met een aparte garage of een tuinperceel, een hoeve met weiden, een villa op twee kadastrale percelen: het puntperceel op het adres is dan maar een deel van het pand. De API lost dit op in twee stappen.

**Stap 1: opvragen zonder `capakeys`.** De parcel-sectie bevat het puntperceel (`main_parcel`, `parcels[]` met 1 item) en `parcel_candidates[]`: de aangrenzende percelen (maximaal 12), elk met `capakey`, kadastrale `area_m2`, `direction` (windrichting ten opzichte van het hoofdperceel, altijd `N`, `E`, `S` of `W` ongeacht de taal, zoals `orientation_garden`), `built` (true/false, of `null` wanneer de gewestelijke gebouwenlaag tijdelijk niet beschikbaar was) en `distance_m`. Toon die lijst aan de gebruiker (bijvoorbeeld "33011I0172/00G000, 40 m², zuid, bebouwd" = de garage) en laat hem aanvinken wat bij het pand hoort. Er zijn geen eigenaarsgegevens; de gebruiker kent het 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}'
```

Live antwoord van 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}
  ]
}
```

**Stap 2: opnieuw opvragen met `capakeys`** (het hoofdperceel plus de aangevinkte percelen, kommagescheiden, maximaal 10; de vorm met streepje wordt aanvaard). Zelfde gebruiker, zelfde adres: het in stap 1 geleverde basispakket wordt **niet opnieuw aangerekend** (melding `dedup` met `section: basic`; `billing.free_reason` is enkel `dedup` wanneer niets nieuws geleverd wordt, anders `null` (aangerekend), `test` of `pilot`), ook al wordt de parcel-sectie met de nieuwe percelen opnieuw opgebouwd. Voegt u in stap 2 de AVM-optie toe, dan betaalt u enkel die optie.

```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}'
```

Live antwoord van 07-09-2026 (`request_id` 2e88e0e1cfc66d6731aa1ca13f5ae83d, 6,8 s; `free_reason` is hier `dedup` omdat deze gebruiker eerder die dag al `avm` voor dit pand kreeg):

```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"
}
```

Regels en foutpaden:

- De velden op sectieniveau (`capakey`, `area_m2`, `zoning`, `preemption_right`, ...) blijven die van het **hoofdperceel**: het opgegeven perceel waarop het adrespunt valt, anders het eerste opgegeven perceel (dan melding `main_parcel_not_in_capakeys` met het puntperceel, zodat u het niet vergeet). `zoning_combined` is één object wanneer alle percelen dezelfde planologische bestemming hebben, anders een lijst met `parcels[]` per bestemming. `preemption_right_combined.status` is `yes` zodra één perceel in een perimeter ligt (met de capakeys in `parcels[]` en het detail per perceel in `details`).
- De AVM (type house) gebruikt `total_area_m2`; `avm.inputs_used.plot_area_source` zegt `cadastre_main_parcel`, `cadastre_<n>_parcels` (n = aantal geleverde percelen, 2 tot 10, bijvoorbeeld `cadastre_2_parcels`) of `partner`; het sectieveld `parcel.plot_area_source` draagt enkel de twee kadasterwaarden, `partner` komt enkel in `avm.inputs_used` voor. Voor een appartement gaat geen grondoppervlakte naar het model (ook niet met `capakeys`).
- `plot_area_m2` is een terugval: zonder `capakeys` en met uw eigen totaal gebruikt de AVM dat (melding `plot_area_not_cadastral`); met `capakeys` wint altijd het kadaster.
- `422 capakey_invalid`: verkeerde vorm of meer dan 10 sleutels (`details[]` noemt de foute delen). `422 capakey_too_far`: een perceel ligt verder dan 2 km van het adres; niets wordt geleverd of aangerekend (misbruikrem: percelen ver van het adres horen niet bij het pand). De parcel-sectie zit in het basispakket, dus `capakeys` vereist geen extra sectie (`400 invalid_request` enkel wanneer uw plan de parcel-sectie niet toelaat).
- Een capakey die niet in het percelenplan bestaat: melding `capakey_not_found` (sectie parcel, de boodschap noemt het perceel) en `parcels_not_found[]`; de andere percelen worden geleverd. Faalt de perceelanalyse voor één perceel, dan telt enkel zijn kadastrale oppervlakte (melding `parcel_analysis_incomplete`).
- Dezelfde `capakeys` opnieuw binnen 30 dagen geeft het bewaarde antwoord uit het grootboek terug (`from_cache: true`); andere `capakeys` geven een verse parcel-sectie, eveneens gratis (zelfde pand). Dezelfde `Idempotency-Key` met andere `capakeys` = `409 idempotency_conflict`. `parcel_candidates`, `parcel_candidates_source` en `parcel_candidates_remark` zijn altijd aanwezig: `null` wanneer `capakeys` meegegeven werd; `parcel_candidates_remark` is ook `null` wanneer de burenbevraging lukte.
- Belasting: beperk u tot de percelen van het dossier. Elke aanvraag met `capakeys` doet één kadasterbevraging plus één perceelanalyse per perceel (tot 10); een hoeve met 3 percelen duurt 3 tot 12 s; de eerste analyse van een perceel waarvan de gewestelijke lagen koud zijn kan tot 21 s duren (het budget van de parcel-sectie). Wordt dat budget overschreden, dan blijft het antwoord 200, ontbreekt de parcel-sectie (`upstream_errors.parcel: timeout`, melding `section_missing`); het basispakket wordt één keer aangerekend: vraag na enkele seconden opnieuw op met een nieuwe `Idempotency-Key`; de herhaling is gratis (dedup) en bouwt de parcel-sectie opnieuw op.

## 4. Prijskaart (sectie `price_map`)

Het basispakket (`sections=basic`) bevat de prijskaart: vraagprijsniveaus per buurt binnen 3.000 m rond het adres (maximaal 40 buurten) als **data plus GeoJSON-geometrie**. Taxon levert geen kaartbeelden of tegels; u tekent de polygonen zelf met uw eigen kaartbibliotheek en -licentie (Leaflet, MapLibre, Mapbox, ...) en kleurt ze op `price_per_m2_house` of `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}'
```

Live antwoord van 07-09-2026 (`request_id` 88915700832ddc745b4355de3c15f332, 0,3 s; melding `price_map_sparse` omdat de buurt van het pand maar 5 advertenties heeft):

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

Vorm van het blok: `{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}`; per buurt `{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}` waarbij `geometry` een GeoJSON Polygon of MultiPolygon is (WGS84 lon/lat). Blokgrootte 20 tot 50 KB; warm antwoord onder 0,5 s, de eerste aanvraag na een herstart van de dienst kan tot 20 s duren. Tekenen met 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);
```

Regels: `low_sample: true` = minder dan 30 Taxon-advertenties van de laatste 24 maanden in die buurt, als indicatief tonen (bijvoorbeeld gearceerd); melding `price_map_sparse` (sectie `price_map`) wanneer de buurt van het pand zelf minder dan 30 advertenties heeft, de sectie wordt wel geleverd; ligt er binnen 3 km geen enkele buurt met prijzen, dan is de sectie `null`, wordt de melding `price_map_unavailable` toegevoegd, is `upstream_errors.price_map` gelijk aan `no_coverage`; geen storing, het basispakket blijft aangerekend. Toon de `source`-regel naast de kaart. De geometrie valt onder dezelfde gebruiksregels als de rest van de bundel (per dossier, geen afgeleide kaartlaag). De prijskaart zit in het basispakket: geen aparte prijs, de pakketprijs staat in `billing.price.package` en in de CSV-kolom `price_package`; dedup volgt het pakket.

## 5. Foto's

Elk referentiepunt draagt maximaal 5 objecten `photos[] {url, download_token, valid_until}`, hoofdfoto (gevel) eerst, lege lijst zonder foto's.

- `url` is een getekende capability-URL op taxonapi.be, momenteel `https://taxonapi.be/api/v1/marketexplorer/foto/<token>?w=640`. `w` is de serverbreedte: 160, 320 of 640 (andere waarden worden naar boven afgerond en begrensd op 640; zonder `w` levert de link ook 640 px), altijd JPEG; het origineel wordt nooit geleverd. Ze werkt zonder sleutel, rechtstreeks in een `<img>`, met `Cache-Control: public, max-age=1800, immutable`. Niet parseren, niet zelf samenstellen, binnen het uur ophalen.
- `valid_until` (UTC) is 1 uur na uitgifte. Na verloop of bij manipulatie antwoordt de URL HTTP 404 met `text/plain`-body `invalid_or_expired_token` (live gemeten: een gewijzigd token geeft onmiddellijk 404).
- Bij een herhaalde aanvraag (dedup of Idempotency-Key-replay) worden de fotolinks vernieuwd: nieuwe `url` en `valid_until`, zelfde `download_token`. Vraag de bundel dus opnieuw op in plaats van links te bewaren.
- `download_token` is een stabiel opaak kenmerk (20 hex) voor uw eigen boekhouding of dedup; het haalt niets op.
- **Inbedden in het rapport (2.5.0).** Haal foto's op binnen de geldigheid van de link (1 uur) en gebruik ze enkel voor het dossier waarvoor de aanvraag gebeurde. Ze inbedden in het schattingsrapport (PDF) van dat dossier mag, altijd met een zichtbare bronvermelding bij elke foto (de `attribution` uit het antwoord, bijvoorbeeld "Referenties: Taxon (taxon.be)"), onder de voorwaarden van de partnerovereenkomst (de partner vrijwaart Taxon voor claims van portalen of makelaars over de foto's). Geen herpublicatie, geen bewaring buiten dat rapport, geen bulkdownload; de resolutie blijft maximaal 640 px (`w=640`). Haal het beeld serverzijdig op bij het samenstellen van het rapport; bewaar de link niet, hij vervalt na een uur.

**Kenmerken, samenvatting en thumbnail (2.2.0).** Elk referentiepunt draagt ook `features` (gestructureerde kenmerken van de advertentie: garage, parking_spaces, terrace en terrace_m2, garden en 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; elke sleutel aanwezig, `null` = onbekend, enum-waarden Engels in elke taal), `summary` (een zin in de woorden van Taxon, enkel opgebouwd uit die velden, in de taal van `Accept-Language`; nooit advertentietekst) en `photo_count` (totaal aantal foto's van de advertentie, `photos[]` blijft beperkt tot 5). `thumbnail {url, valid_until}` is de hoofdfoto (voorgevel) op `w=160` onder dezelfde regels als de fotolinks (1 uur, vernieuwd bij een herhaalde aanvraag, niet opslaan); `photos[0]` is dezelfde foto op 640 px; `null` zonder foto's. Toon `summary` als inleidende regel en `features` als chips of een klein tabelletje; behandel `null` als "niet opgegeven", nooit als "nee". Voorbeeld: `"summary": "Rijhuis van 156 m² op een perceel van 259 m² met garage, tuin (190 m²), terras (34 m²) en kelder, 3 slaapkamers, EPC B, bouwjaar 1975."` met `"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` (echt referentiepunt van 07-09-2026).

## 6. Talen

`Accept-Language: nl` (standaard), `fr` of `en` bepaalt de taal van labels (`type_label`, `transaction_label`, `price_kind_label`, `status_label`, `source_label`, `history[].label`, `condition` van een referentiepunt, `epc_source`, `building_stock.distribution[].label`, labels van `amenities`, `confidence.label`), van meldingen en foutboodschappen, van `attribution` en `disclaimer`, van de bronregels (`source`) en van de gemeentenaam (één regel voor `address`, referentiepunten, `neighbourhood.sector`, `neighbourhood.safety` en `price_map`: Vlaanderen Nederlands, Wallonië Frans, Brussel Nederlands voor `nl` en Frans voor `fr` en `en`; geen exoniemen, dus Liège en Braine-l'Alleud ook voor `nl`). JSON-sleutels en enum-waarden (`sale`, `rent`, `asking_price`, `house_number`, `terraced`, `published`, ...) zijn altijd Engels. Het bestemmings-`label` is de tekst van de gewestelijke dienst (Nederlands voor VL, Frans voor WAL) en heeft geen Engelse vertaling; `zoning.category` is een Engelse enum-waarde (`residential`, `residential_rural`, `residential_expansion`, `agricultural`, `industrial`, `nature`, `forest`, `park`, `recreation`, `community_facilities`, `extraction`, `weekend_residence`, `mixed`, `other`). Windrichtingen (`garden_orientation`, `parcel_candidates[].direction`, `features.orientation_garden`) zijn taalonafhankelijk (`N`, `NE`, `E`, ...). De volgorde van `notices[]` is niet betekenisvol. Alle voorbeelden in deze documentatie sturen `Accept-Language: en` (identieke uitvoer in de drie talen); met `nl` krijgt u labels, meldingen en gemeentenamen in het Nederlands (zie T07b).

Live gemeten op hetzelfde Waalse adres (07-09-2026): `transaction_label` for sale / à vendre / te koop; `confidence.label` medium / moyenne / gemiddeld; `garden_orientation` SW in elke taal; `address.municipality` Braine-l'Alleud in elke taal (geen exoniem); `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. Foutafhandeling

Elke fout heeft dezelfde body: `{"error", "message", "request_id", "charged": false}` plus, afhankelijk van de code, `details[] {field, issue}`, `retry_after`, `scope`, `limit`, `section`, `user_ref`, `reason`, `since` of `pilot`. `message` volgt `Accept-Language`. Een fout wordt nooit aangerekend. Header- en parameterfouten worden samen gemeld in één `400 invalid_request` (`details[]` noemt elk fout veld, headers inbegrepen).

| HTTP | error | Betekenis | Wat de client doet |
|---|---|---|---|
| 400 | `invalid_request` | Parameter of header ongeldig; `details[]` noemt het veld (ook een 1.x-parameternaam zoals `adres`, een parameter die tweemaal verstuurd is, een onbekende sectie, `capakeys` zonder de parcel-sectie, een ongeldige `X-User-Ref`, `X-Case-Ref` of `Idempotency-Key`) | Corrigeer de aanvraag; nooit ongewijzigd herhalen |
| 400 | `user_ref_required` | `X-User-Ref` ontbreekt (en geen alias) | Voeg de header toe |
| 400 | `user_ref_conflict` | Twee of meer gebruikersheaders met verschillende waarden | Stuur één header, bij voorkeur `X-User-Ref` |
| 401 | `missing_api_key`, `invalid_api_key` | Sleutel ontbreekt of onbekend | Configuratiefout; verwittig operations, niet herhalen |
| 403 | `key_revoked` | Sleutel ingetrokken | Schakel over op de nieuwe sleutel |
| 403 | `ip_not_allowed` | IP buiten de allowlist | Geef het nieuwe server-IP door aan Taxon |
| 403 | `client_suspended` | Klant geschorst | Contacteer Taxon; niet herhalen |
| 403 | `section_not_allowed` | Sectie (`section`) niet in uw plan | Verwijder de sectie |
| 403 | `quota_exceeded` | Dagplafond; `scope` `day` (sleutel, `limit` 300; testsleutel 20) of `user_day` (`user_ref`, `limit` 20); header `Retry-After` = seconden tot middernacht Europe/Brussels | In wachtrij zetten tot `Retry-After`; andere gebruikers blijven werken |
| 403 | `pilot_exhausted` | Pilotquota of -periode voorbij; `pilot{}` | Contacteer Taxon |
| 403 | `user_suspended` | Gebruiker geschorst (`reason` `raster_scan`, `handmatig` of tekst, `since`); geen Retry-After, geen automatisch verval | Blokkeer die gebruiker in uw UI; opheffing via info@taxon.be met `user_ref` |
| 404 | `address_not_found` | Geen enkele geocoder vindt het adres | Laat de gebruiker het adres corrigeren |
| 404 | `not_found` | Onbekend pad (ook de 1.x-paden `/adres`, `/verbruik`) | Corrigeer het pad |
| 405 | `method_not_allowed` | Geen GET | Gebruik GET |
| 409 | `idempotency_conflict` | Zelfde `Idempotency-Key` voor een andere aanvraag | Gebruik een nieuwe sleutel |
| 422 | `address_imprecise` | Enkel gevonden op straat- of gemeenteniveau, of zonder postcode en gemeente | Vraag het huisnummer en de postcode |
| 422 | `type_unsupported` | Type niet ondersteund voor dit adres | Wijzig het type |
| 422 | `capakey_invalid` | Vorm of meer dan 10 (`details[]`) | Corrigeer de capakeys |
| 422 | `capakey_too_far` | Een perceel verder dan 2 km | Verwijder dat perceel |
| 429 | `rate_limited` | Minuutplafond (applicatie); `Retry-After`-header en `retry_after` | Wacht, probeer opnieuw met dezelfde `Idempotency-Key` |
| 429 | `rate_limit_exceeded` | Minuutplafond (nginx); body `{"error": "rate_limit_exceeded", "retry_after": 60}`, geen header, geen `request_id` | Wacht `retry_after` uit de body, probeer opnieuw |
| 502 | `upstream_unavailable` | Basic-sectie kon niet geleverd worden; `details[]` noemt het blok | Tot 3 retries met back-off (2, 4, 8 s), zelfde `Idempotency-Key` |
| 503 | `overloaded` | Overbelastingswacht; `Retry-After` | Wacht `Retry-After`, probeer opnieuw met dezelfde `Idempotency-Key` |
| 504 | `timeout` | Bundel langer dan 40 s | Probeer opnieuw met dezelfde `Idempotency-Key` |
| 500 | `internal_error` | Onverwacht | Eén keer opnieuw proberen, meld daarna de `request_id` |

Regels die u in code afdwingt:

- Client-timeout minstens 60 s; enkel opnieuw proberen bij 429 (met `Retry-After` of `retry_after`), 503 en 5xx (502/504/500), altijd met dezelfde `Idempotency-Key`; herken een herhaald antwoord aan `X-Idempotent-Replay: true`; dezelfde sleutel met eender welke andere invoer (adres, gebruiker, secties, AVM-parameters, `capakeys` of taal) geeft 409 `idempotency_conflict`; `X-Request-ID` is geen idempotentiesleutel (nginx overschrijft ze). Bij `403 quota_exceeded` wacht u de `Retry-After` af.
- Negeer onbekende JSON-velden en onbekende meldingscodes; vang ontbrekende secties (`parcel`, `avm`, `price_map`) op via `sections_delivered`, `notices[]` (bewust niet geleverd: `living_area_required`, `parcel_not_found`, `avm_insufficient_data`, `price_map_unavailable`) en `upstream_errors` (storing `timeout` of `upstream_error`, melding `section_missing`; voor `price_map` ook `no_coverage`). De aanrekening volgt het pakket (2.4.0): een ontbrekende `parcel` of `price_map` laat het basispakket aangerekend, een ontbrekende `avm` wordt niet aangerekend. `basic.comparables` is een lijst van blokken per type en transactie (`sale`, `rent`); een deelblok van `neighbourhood` kan `null` zijn. Bij `upstream_errors.parcel: timeout` (koude perceelanalyse) vraagt u na enkele seconden opnieuw op met een nieuwe `Idempotency-Key`; het basispakket wordt dan niet opnieuw aangerekend (dedup) en de ontbrekende sectie wordt gratis opnieuw opgebouwd.
- Percelen: toon `parcel.parcels[]` en `total_area_m2` in het rapport wanneer `capakeys` verstuurd werden, niet enkel de velden op sectieniveau van het hoofdperceel; `parcel_candidates[]` is een keuzelijst voor de gebruiker, geen uitspraak over eigendom. Stuur nooit `capakeys` uit een ander dossier en laat kandidaten die de gebruiker niet aanvinkte weg vóór stap 2.
- Toon `attribution` en `disclaimer` uit het antwoord naast de gegevens en in het rapport; `amenities.attribution` naast de voorzieningen; `source` naast perceel, gebouwenpark, veiligheid en prijskaart.
- Wis ruwe antwoorden na maximaal 30 dagen (job); enkel het afgewerkte rapport blijft in het dossierarchief.
- Rasterscans (opeenvolgende huisnummers of postcodes in korte tijd) schorsen de gebruiker: `403 user_suspended` met `user_ref`, `reason` en `since`, geen Retry-After, geen automatisch verval; opheffing enkel door Taxon (info@taxon.be); de schorsing treft de gebruiker, niet de klant; 3 of meer geschorste gebruikers binnen 24 u = `client_suspended` voor de hele sleutel. Bouw dus nooit een bulk- of testlus over echte adressen buiten het testplan.
- `X-User-Ref` = de echte gebruiker-id, stabiel, tekenset `A-Z a-z 0-9 . _ : @ -` (maximaal 64; de API normaliseert naar kleine letters); nooit één vaste waarde voor alle gebruikers (dat is dedup-misbruik en leidt tot schorsing).
- Log per aanvraag `request_id`, gebruiker, dossier, `sections_delivered`, `billing.charged`, `billing.price.package`, `billing.price.avm`, `billing.price.total`: dan sluit uw boekhouding aan op `GET /usage` en de maandfactuur.

## 10. Idempotentie

Stuur met elke aanvraag een verse UUID als `Idempotency-Key` en hergebruik ze enkel voor retries van diezelfde aanvraag. Dezelfde sleutel binnen 24 uur geeft het bewaarde antwoord byte voor byte terug (zelfde `request_id`, antwoordheader `X-Idempotent-Replay: true`) zonder nieuwe aanrekening en zonder telling tegen het dagplafond; fotolinks in de replay krijgen een verse geldigheid. De replay dekt bewaarde 200-antwoorden; na een fout (4xx of 5xx) voert dezelfde sleutel de aanvraag gewoon opnieuw uit (dat is wat een retry na 502/503/504 nodig heeft), en elke uitgevoerde poging telt mee in de dagplafonds zoals beschreven in sectie 1. Dezelfde sleutel met een ander adres, een andere gebruiker, een andere sectielijst, een andere AVM-parameter, andere `capakeys` of een andere taal geeft `409 idempotency_conflict` (niet aangerekend): gebruik een nieuwe sleutel. Live gemeten op 08-09-2026: de replay van het hoofdvoorbeeld (Doorniksestraat 40, Kortrijk) gaf `request_id` df0cfcebf101496f99e7f01a0f14f227 terug met `X-Idempotent-Replay: true` in 80 ms, body byte voor byte identiek; dezelfde sleutel met een ander adres gaf 409 `idempotency_conflict` (niet aangerekend).

Dedup is een ander mechanisme (commercieel, 30 dagen, per gebruiker en pand, per pakket of optie) en werkt zonder enige header: dezelfde gebruiker die hetzelfde pand opnieuw opvraagt, krijgt `from_cache: true` en `free_reason: dedup` wanneer alle gevraagde secties al geleverd waren, of meldingen `dedup` per sectie wanneer dat maar voor een deel het geval was. Een pand is het gegeocodeerde punt (afgerond op ongeveer 1 m) plus het busnummer: een ander `type`, andere AVM-parameters, andere `capakeys` of een andere taal maken geen ander pand. Dedup bevriest niets: zo'n herhaling wordt met de nieuwe invoer opnieuw opgebouwd (verse labels, verse `inputs_used`) en blijft gratis voor de al geleverde secties; `free_reason` is `dedup` zodra elke geleverde sectie al geleverd was (grootboektreffer of herbouw, ook met een testsleutel), anders `test`, `pilot` of `null` (aangerekend) met meldingen `dedup` per sectie. Een aanvraag zonder resultaat (0 referentiepunten, `free_reason: no_result`) start geen dedup-venster: de volgende aanvraag voor hetzelfde pand door dezelfde gebruiker wordt opnieuw opgebouwd en gewoon aangerekend zodra er referentiepunten zijn (een AVM-optie die toen wel geleverd werd, houdt wel haar eigen venster).

## 11. Pilot-testplan (testsleutel tx_test_)

Testsleutel: maximaal 20 aanvragen per dag, nooit gefactureerd, antwoord bevat `"environment": "test"` en een melding `test_environment`. Spreid de tests over enkele dagen of vraag een tijdelijk hoger testplafond. De 20 tellen zoals beschreven in sectie 1 (dedup-treffers en 404/422-adresfouten inbegrepen; Idempotency-Key-replays niet). De verwachte waarden hieronder zijn live gemeten op 07-09-2026 met een testsleutel.

### Testadressen (3 per gewest)

| Gewest | Adres | type | living_area_m2 | Verwacht |
|---|---|---|---|---|
| WAL | Avenue Alphonse Allard 94, 1420 Braine-l'Alleud (geverifieerd door Taxon) | house | 165 | region WAL, nis_code 25014, capakey 25744E0226/00_000, zoning plan "plan de secteur" label "Habitat", garden_orientation SW, melding 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, referentiepunten type apartment |
| BXL | Avenue Louise 200, 1050 Ixelles | apartment | 110 | region BXL, zoning plan GBP/PRAS, melding avm_indicative_not_regionally_calibrated |
| BXL | Rue Royale Sainte-Marie 22, 1030 Schaerbeek (geverifieerd door 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 (geverifieerd door Taxon) | house | 150 | region VL, nis_code 33011, geocoder geo.api.vlaanderen.be, zoning plan gewestplan, geen indicatieve melding |
| VL | Kortrijksesteenweg 300, 9000 Gent | apartment | 90 | region VL, nis_code 44021 |
| VL | Mechelsesteenweg 100, 2018 Antwerpen | apartment | 75 | region VL, nis_code 11002 |

Drie adressen (één per gewest) werden door Taxon geverifieerd tegen BeSt; de andere dienen om geocoding, gewestdetectie en de sectielogica te testen. Bestaat een huisnummer niet in BeSt (404 `address_not_found`), kies dan een naburig nummer in dezelfde straat (de aanrekening is in test toch nul).

### Scenario's (afvinken)

| # | Scenario | Oproep | Verwacht |
|---|---|---|---|
| T01 | Health | `GET /health` zonder sleutel | 200, `status: ok`, `versie: "2.5.1"` (dienstversie, contract 2.0), `bundel: live`, `db: ok`; `POST /address` = 405 `method_not_allowed`; onbekend pad en de 1.x-paden `/adres`, `/verbruik` = 404 `not_found` |
| T02 | Geen sleutel | `GET /address` zonder `X-Api-Key` | 401 `missing_api_key` |
| T03 | Verkeerde sleutel | `X-Api-Key: tx_test_0000...` | 401 `invalid_api_key`, zelfde antwoordtijd als T02 |
| T04 | Geen gebruiker | geldige sleutel, zonder `X-User-Ref` (en zonder alias) | 400 `user_ref_required` (boodschap in de taal van `Accept-Language`) |
| T04b | Verouderde alias | enkel `X-Gebruiker-Ref: ag-0417` of `X-Kantoor-Ref: AG-0417` | 200, `billing.user_ref: "ag-0417"` (alias werkt, zelfde normalisatie) |
| T04c | Alias-conflict | `X-User-Ref: ag-0417` en `X-Kantoor-Ref: ag-0022` | 400 `user_ref_conflict`, `details[0].field: "X-User-Ref"`, niet aangerekend; beide gelijk (ook `AG-0417`) = 200 |
| T05 | Validatie | `address=abc` (te kort), `type=villa`, `living_area_m2=7`, `X-User-Ref: ag 04/17`, oude naam `adres=` | 400 `invalid_request` met `details[]` (alle foute velden in één antwoord, headers inbegrepen; `field` = `address`, `type`, `living_area_m2`, `X-User-Ref`, of `adres` met issue "Extra inputs are not permitted"; `:` en `@` zijn toegelaten in de gebruikersreferentie) |
| T06 | Basic WAL | Braine-l'Alleud, `sections=basic` | 200, `sections_delivered: ["basic", "parcel", "price_map"]` (basispakket), `comparables` = lijst van blokken (sale en rent, elk met `address_mode`) met <= 25 items, elke `price_kind = asking_price`, `source = listing`, `epc_source = "as advertised"`, geen portaalnaam of link, `safety` met cijfers per 1.000, `building_stock.distribution` met categorieën residential/commerce_services/industry/agriculture/other, `upstream_errors: {}`, `environment: test` |
| T07 | Labels FR | zelfde als T06 met `Accept-Language: fr` | `attribution = "Références : Taxon (taxon.be)"`, disclaimer in het Frans, meldingen in het Frans, `transaction_label: "à vendre"`, `confidence.label: "moyenne"` (met avm), `garden_orientation: "SW"` (met parcel) |
| T07b | Labels NL | zelfde met `Accept-Language: nl` | `attribution = "Referenties: Taxon (taxon.be)"`, `address.municipality: "Braine-l'Alleud"`, `transaction_label: "te koop"` |
| T08 | AVM zonder bewoonbare oppervlakte | `sections=basic,avm` zonder `living_area_m2` | 200, `avm` niet in `sections_delivered`, melding `{code: living_area_required, section: avm}`, `billing.price.avm` = 0 |
| T09 | AVM met bewoonbare oppervlakte | `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"`; melding `avm_indicative_not_regionally_calibrated` (WAL/BXL), niet voor VL |
| T10 | Perceel per gewest | `sections=basic` op één adres per gewest (parcel zit in het basispakket) | `capakey` ingevuld, `cadastral_area_m2`, `fiscal_situation` (2.4.2, bv. `2027-01-01`), `zoning.plan` = plan de secteur / GBP/PRAS / gewestplan of RUP, `preemption_right.status` (`none`, `yes` of `unknown`), `source` ingevuld, geen overstromingsveld, geen geometrie; `main_parcel`, `parcels[]` (1 item), `parcel_candidates[]` (<= 12, elk met `direction` N/E/S/W en `built`), `parcel_candidates_remark: null` |
| T10b | Meerdere percelen (rijwoning + 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` en `plot_area_source: "cadastre_2_parcels"`, `bedrooms_filter.applied: true`; opgevraagd door dezelfde gebruiker na stap 1 van sectie 3 op Rijselstraat 62: melding `dedup` voor `basic` (basispakket), `free_reason: test` (testsleutel; `avm` is hier nieuw, met een live-sleutel wordt enkel `avm` aangerekend en is `free_reason` `null`; `dedup` enkel wanneer niets nieuws geleverd wordt; T10 zelf gebruikt Rijselstraat 60, een ander pand) |
| T10c | Hoeve met 3 percelen | `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` = één object (category `agricultural`, label Landbouwgebied, plan gewestplan), `preemption_right_combined.status: none`, `plot_area_source: cadastre_3_parcels`, duur < 15 s (gemeten 4,4 s) |
| T10d | Capakey-foutpaden | (1) `capakeys=FOUT`; (2) 11 sleutels; (3) `capakeys=33011I0172/00H000,33015C0813/00K000` op Rijselstraat 62 Ieper; (4) `capakeys=33011I0172/00H000,33011I9999/00Z000`; (5) `capakeys=...` met `sections=basic` | (1) en (2) 422 `capakey_invalid` met `details[]` (issue "invalid: FOUT" resp. "11 given, maximum 10"); (3) 422 `capakey_too_far` ("lies 7474 m from the address"), niet aangerekend; (4) 200 met melding `capakey_not_found` en `parcels_not_found: ["33011I9999/00Z000"]`, 1 perceel geleverd; (5) 200, perceel geleverd (de parcel-sectie zit in het basispakket) |
| T10e | plot_area_m2 en bedrooms | T09 met `&plot_area_m2=850&bedrooms=3` (zonder capakeys) | `avm.inputs_used.plot_area_m2: 850.0`, `plot_area_source: "partner"`, melding `plot_area_not_cadastral`; blokveld `bedrooms_filter` (`applied` true bij >= 8 referentiepunten met 2-4 slaapkamers, anders melding `bedrooms_filter_dropped`); `inputs_used.bedrooms: 3` |
| T10f | Prijskaart | `sections=basic` (prijskaart in het basispakket) op Braine-l'Alleud, Ieper (Rijselstraat 60) en Schaarbeek | 200, `price_map` in `sections_delivered`, `radius_m: 3000`, `price_kind: asking_price`, `reference_period: "2026-03-30"`, `subject_neighbourhood` = de `id` van het item met `is_subject: true` (Braine-l'Alleud "5906" Saint-Sebastien, Ieper "7220" Ieper-Centrum, Schaarbeek "3110"), `neighbourhoods` 40 / 38 / 40 items elk met `geometry.type` Polygon of MultiPolygon, precies één `is_subject: true`; Braine-l'Alleud draagt melding `price_map_sparse`; `billing.price.package` aanwezig; of `price_map: null` met melding `price_map_unavailable` en `upstream_errors.price_map: no_coverage` (pakket ongewijzigd) |
| T11 | Dedup, 3 keer klikken | T06 drie keer met dezelfde `X-User-Ref` | 1e: `free_reason: "test"` (testsleutel, prijs 0), 2e en 3e: `from_cache: true`, `free_reason: "dedup"`, `dedup_of` = request_id van de 1e, melding `dedup` met `section: null` |
| T12 | Dedup extra sectie | na T11: `sections=basic,avm&living_area_m2=165` | enkel `price.avm` > 0 (live) of gemarkeerd als nieuw; `price.package` = 0 en melding `dedup` met `section: basic` (basispakket al geleverd) |
| T13 | Andere gebruiker | T06 met `X-User-Ref: ag-0022` | `dedup_of: null`, nieuwe aanrekening |
| T14 | Idempotentie | T09 tweemaal met dezelfde `Idempotency-Key` | identieke body, zelfde `request_id`, de tweede draagt header `X-Idempotent-Replay: true` en telt niet in `billing.today` |
| T14b | Idempotentie-conflict | zelfde `Idempotency-Key` als T14 met een ander adres | 409 `idempotency_conflict`, niet aangerekend |
| T15 | Adres onbekend | `address=Rue Inexistante 999, 1420 Braine-l'Alleud` | 404 `address_not_found`, `charged: false` |
| T15b | Adres zonder huisnummer | `address=Avenue Alphonse Allard, 1420 Braine-l'Alleud` | 422 `address_imprecise`, niet aangerekend |
| T16 | Sectie buiten plan | `sections=basic,report` | 400 `invalid_request` (onbekende sectie, `details[0].field: sections`) of 403 `section_not_allowed` met `section` (bekend maar niet in het plan; om de 403 te testen vraagt u Taxon het plan van uw testklant tijdelijk te beperken; uw plan is zichtbaar in `GET /usage` onder `plan.sections_allowed`) |
| T17 | Dagplafond test | 21e aanvraag op één dag | 403 `quota_exceeded` met `scope: "day"`, `limit: 20` (testsleutel; live: 300 per sleutel, 20 per gebruiker met `scope: "user_day"` en `user_ref`), header `Retry-After` (tot middernacht); `billing.today.client_today` klom vooraf tot 20 (geteld: elke aanvraag die de adresopzoeking bereikt, zie sectie 1; Idempotency-Key-replays niet) |
| T18 | Minuutplafond | 80 aanvragen in 1 minuut (af te spreken met Taxon; kost testquota) | 429: vanuit de app `rate_limited` met `Retry-After`, of vanuit nginx `{"error":"rate_limit_exceeded","retry_after":60}` zonder header; geen 5xx |
| T19 | Gebruik JSON | `GET /usage?month=<this month>` | `environment: test`, `per_user` bevat ag-0417 en ag-0022 met `free_dedup` en `sections{package, avm}`, `sections_delivered{basic, parcel, avm, price_map}` (ook `total.sections_delivered`), blok `quality{error_pct, latency_p50_ms, latency_p95_ms, upstream_errors}`, `amount` 0; `?user_ref=AG-0417` filtert (hoofdletterongevoelig); `?maand=` = 400 met `details[0].field: maand`; blok `plan {sections_allowed, max_per_minute, max_per_day, max_per_user_per_day, test_max_per_day, dedup_days, idempotency_hours, address_mode}` (uw plan zonder prijzen); `pilot.expired` aanwezig; blok `per_key[]` (2.4.1) met uw testsleutel (`environment: test`, `active: true`, `revoked_at: null`, sommen = `total`) |
| T20 | Gebruik CSV | `format=csv` | `Content-Type: text/csv; charset=utf-8`, BOM, `;`, elk veld tussen aanhalingstekens, één rij per aanvraag uit T06-T14, 16 kolommen `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` sinds 2.4.1), `Content-Disposition`-bestandsnaam `taxon-usage-<client>-<month>.csv` |
| T21 | Fotolinks en inbedden | open een `photos[].url` uit T06 vóór en na `valid_until`; haal één foto serverzijdig op binnen het uur en bed ze in een testrapport van dat dossier in | eerst 200 (image/jpeg, `Cache-Control: public, max-age=1800, immutable`), daarna 404 `invalid_or_expired_token`; URL met een gewijzigd token = 404; de ingebedde foto draagt een zichtbare bronvermelding (de `attribution` uit het antwoord) ernaast, is maximaal 640 px en verschijnt enkel in het rapport van dat dossier (2.5.0, sectie 5) |
| T22 | Kenmerken en thumbnail | uit T06 een item van `basic.comparables[].items[]` | `features` met alle 22 sleutels (onbekend = `null`), `summary` een niet-lege zin in de gevraagde taal zonder portaalnaam, `photo_count` >= `len(photos)`; als `thumbnail` niet `null` is: GET van `thumbnail.url` = 200 `image/jpeg` en het tokendeel van de URL is gelijk aan dat van `photos[0].url` (zelfde foto, andere breedte); met `type=land` (T24) heeft `features` exact de 4 sleutels `plot_area_m2`, `zoning`, `flood_zone`, `land_type` |
| T23 | Time-out en retry | forceer een client-timeout van 1 s, probeer dan opnieuw met dezelfde `Idempotency-Key` | de tweede poging levert het antwoord zonder dubbele telling |
| T24 | Grond VL | `address=Buissestraat 17, 9690 Kluisbergen&type=land&sections=basic,avm` (zonder `living_area_m2`) | 200, `sections_delivered: ["basic", "parcel", "price_map"]`, `avm: null`, melding `{code: avm_not_available_for_land, section: avm}`, `billing.price.avm` = 0 en geen `living_area_required`; één referentieblok met `type: land`, `transaction: sale`, `radius_m: 5000` (gemeten 25 items), elk item `living_area_m2: null`, `plot_area_m2` en `price_per_m2_plot` gevuld als het zoekertje een grondoppervlakte draagt, `features` met de 4 sleutels (`land_type` `building_plot`, `project_land` of `other`), `summary` beginnend met "Building plot", "Development land" of "Land", geen landbouwgrond, weiden, bos, industriegrond of staanplaatsen tussen de items; `neighbourhood.land_price_level.count` > 0 met `p25 <= median <= p75` (gemeten 116 zoekertjes, mediaan 160, na de patch van 15-09-2026), `price_level: null`, `epc_prices: null`, melding `housing_stats_not_available_for_land`; `parcel` en `price_map` zoals bij een huis |
| T24b | Grondaliassen en validatie | `type=grond` op Rue de Fer 12, 5000 Namur (`Accept-Language: fr`), `type=terrain` op Rue Royale Sainte-Marie 22, 1030 Schaerbeek, `type=bouwgrond` op Rijselstraat 62, 8900 Ieper; `type=kasteel` | 200 met `comparables[0].type: "land"` en `type_label` in de gevraagde taal (`terrain` voor fr, `grond` voor nl, `land` voor en), samenvattingen in die taal ("Terrain à bâtir de 1085 m² à Namur, proposé depuis le 10-09-2026."); `type=kasteel` = 400 `invalid_request` met `details[0].issue` "type must be one of house, apartment, land" |
| T24c | Grond, dun gebied | `type=land` in een landelijke gemeente (bijvoorbeeld Hauptstrasse 2, 4760 Büllingen) | `radius_m: 10000` als er minder dan 10 grondzoekertjes binnen 5 km liggen (gemeten: 25 binnen 10 km); bij minder dan 10 binnen 10 km `low_sample: true` op het blok en melding `low_sample` (sectie basic); 0 zoekertjes = melding `no_comparables`, pakket niet aangerekend (`free_reason: no_result`) |
| T24d | Grond, alle publicatiejaren (2.5.1) | `address=Buissestraat 17, 9690 Kluisbergen&type=land` | blok `max_age_months: null`; elk item draagt `age_months` (geheel getal >= 0, hele kalendermaanden sinds `published`); de items van de laatste 24 maanden (`age_months` < 24) komen eerst, op afstand, daarna de oudere, op afstand (maximaal 25 per blok: in een dicht gebied zoals Kluisbergen zijn alle 25 recent, gemeten 92 recente zoekertjes binnen 5 km; oudere zoekertjes verschijnen waar er minder dan 25 recente zijn, bijvoorbeeld Klosterstraße 12, 4700 Eupen: gemeten 16-09-2026, 14 van de 25 items met `age_months` >= 24, pool binnen 5 km 11 recente en 16 oudere, `land_price_level.max_age_months` 60 met `count` 11; of Ooststraat 12, 8647 Lo-Reninge: 13 items waarvan 9 ouder, `max_age_months` 60 met `count` 10); `neighbourhood.land_price_level.max_age_months` is 24, of 60 als er minder dan 10 bruikbare zoekertjes binnen 24 maanden liggen; bij 0 zoekertjes luidt de melding `no_comparables` "Geen grondzoekertjes binnen 10 km (alle publicatiejaren)." |

Afsluiten van de pilottest: bezorg Taxon de lijst van `request_id`'s per scenario; Taxon toetst ze aan het grootboek en bevestigt schriftelijk. Daarna wordt de live sleutel uitgereikt (na ondertekening van de overeenkomst).

## 12. Licentie en gebruiksregels

Samenvatting van de contractuele regels die de API ook technisch afdwingt; de overeenkomst primeert.

- **Per dossier.** Toon de gegevens enkel aan de schatter en neem ze enkel op in het expertiserapport van het dossier waarvoor de aanvraag gebeurde; elke aanvraag draagt de echte gebruiker (`X-User-Ref`) en het dossier (`X-Case-Ref`, contractueel verplicht, technisch optioneel).
- **Bewaring maximaal 30 dagen** voor ruwe antwoorden, logs en caches; enkel het afgewerkte rapport (PDF) blijft in het dossierarchief.
- **Geen afgeleide databank**: gegevens (referentiepunten, statistieken, percelen, prijskaart-geometrie) uit meerdere aanvragen niet samenvoegen, indexeren of opslaan in uw eigen databank, kaartlaag, index of prijsmodel; **geen modelvoeding** (trainen, kalibreren, valideren van eender welk algoritme of AI-systeem); **geen doorverkoop of bulk** (niet publiceren, exporteren, aan derden geven buiten het rapport; niet geautomatiseerd of systematisch bevragen buiten een dossier).
- **Foto's** (2.5.0): opgehaald binnen de geldigheid van de link, enkel gebruikt voor het dossier, enkel ingebed in het rapport van dat dossier met een zichtbare bronvermelding bij elke foto (onder de voorwaarden van de partnerovereenkomst, met inbegrip van de vrijwaring van Taxon door de partner voor claims van portalen of makelaars); geen herpublicatie, geen bewaring buiten dat rapport, geen bulkdownload; maximaal 640 px.
- **Bronvermelding en disclaimer** zichtbaar in de UI en in elk rapport (velden `attribution`, `disclaimer`); `source` per laag en `amenities.attribution` naast die gegevens; white-label enkel via een apart addendum.
- **EPC zoals geadverteerd**, **geen notariële prijzen** (nooit notaris-, VLABEL- of transactiegegevens claimen), **menselijke beslissing** (geen beslissing met rechtsgevolgen uitsluitend op basis van de API of de AVM).
- **Sleutel geheim**, lek gemeld binnen 48 uur; op verzoek (maximaal tweemaal per jaar) een uittreksel van dossiernummers tegenover aanvragen. **De overeenkomst, prijzen en sleutels zijn vertrouwelijk; deze documentatie is openbaar.**

## 13. Checklist vóór go-live

- [ ] Sleutel in een secret store, enkel bereikbaar vanuit de backend; IP-allowlist doorgegeven aan Taxon.
- [ ] `X-User-Ref` gekoppeld aan uw echte gebruikerstabel (zie de definitie in sectie 1); `X-Case-Ref` aan het dossier.
- [ ] Bronvermelding en disclaimer zichtbaar op het scherm en in het rapportsjabloon (in elke taal die u aanbiedt).
- [ ] Bronregels per laag (`source`, `attribution`) naast de gegevens.
- [ ] Wisjob: ruwe antwoorden en logs met API-gegevens ouder dan 30 dagen.
- [ ] Foto's: opgehaald binnen de geldigheid, enkel ingebed in het rapport van het dossier met een zichtbare bronvermelding per foto (2.5.0), geen bulkdownload; geen samenvoeging van referentiepunten of prijskaart-geometrie over dossiers heen.
- [ ] Monitoring op `GET /health` en op het percentage 429/502/503 per dag.
- [ ] Maandelijkse controle van `GET /usage?format=csv` tegenover de Taxon-factuur.
- [ ] Sleutelbeheer in het partnerdashboard (sectie 15): de persoon die een sleutel eenmalig toont en de live-sleutel roteert is gekend; uw client verwerkt de melding `key_rotation_pending` (loggen, binnen 7 dagen van sleutel wisselen).
- [ ] Uw boekhouding leest `billing.price.package` en `billing.price.avm` (2.4.0) en de CSV-kolommen `price_package`, `price_avm`, `price_total`; het basispakket is één lijn per dossier, de AVM-optie een tweede.

## 14. Changelog en mapping 1.x naar 2.0

| Versie | Datum | Wijzigingen |
|---|---|---|
| 2.5.1 | 16-09-2026 | Grond zonder leeftijdsgrens. Bij `type=land` zijn de referenties niet langer beperkt tot zoekertjes van de laatste 24 maanden: alle grondzoekertjes te koop (zelfde subtype-uitsluitingen als voorheen) binnen 5 km (eenmaal uitgebreid tot 10 km bij minder dan 10 in totaal) worden geleverd, eerst de zoekertjes van de laatste 24 maanden (op afstand) en daarna de oudere (op afstand), maximaal 25 per blok, zodat u oudere vraagprijzen binnen het dossier kunt aanpassen aan de marktevolutie (prijsindexering); de licentievoorwaarden blijven gelden: niet samenvoegen, indexeren of opslaan over dossiers heen. Nieuw itemveld `age_months` (hele kalendermaanden sinds `published`, enkel bij `type: land`); het blokveld `max_age_months` is `null` voor grond (blijft 24 voor huis en appartement). `neighbourhood.land_price_level` houdt de laatste 24 maanden aan en verruimt bij minder dan 10 bruikbare zoekertjes binnen 24 maanden naar 60 maanden; zijn `max_age_months` zegt welk venster gebruikt is (24 of 60). De melding `no_comparables` krijgt een eigen grondtekst zonder "(< 24 maanden)". Huis en appartement ongewijzigd. Additief: geen wijziging van paden of bestaande velden. Dienstversie 2.5.1. Patch van 16-09-2026 (zelfde versie 2.5.1): `days_online` is gedocumenteerd als nullable (`null` als de bron geen online-duur registreerde, vooral oudere offline zoekertjes; `last_seen` is dan gelijk aan `published`); het doel van de oudere zoekertjes is geformuleerd als prijsindexering binnen het dossier (licentie ongewijzigd); testscenario T24d gebruikt een voorbeeldadres dat effectief oudere zoekertjes teruggeeft; landantwoorden bewaard onder 2.5.0 worden niet meer uit de invoercache herhaald. |
| 2.5.0 | 15-09-2026 | Gronden en fotoregel. Nieuwe waarde `type=land` (aliassen `grond`, `terrain`, `bouwgrond`) voor bouwgronden: één referentieblok met grondzoekertjes te koop binnen 5 km (eenmaal uitgebreid tot 10 km bij minder dan 10; blokveld `low_sample` en melding `low_sample`), items met `plot_area_m2` en `price_per_m2_plot` (woningvelden `null`, `features` beperkt tot `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} terwijl `price_level` en `epc_prices` `null` zijn (melding `housing_stats_not_available_for_land`); geen AVM voor grond (melding `avm_not_available_for_land`, sectie niet geleverd, niet aangerekend); `living_area_m2` optioneel; `parcel` en `price_map` ongewijzigd; prijs van het basispakket. Fotoregel versoepeld: foto's mogen in het rapport van het dossier ingebed worden met een zichtbare bronvermelding per foto, onder de voorwaarden van de partnerovereenkomst (geen herpublicatie, geen bewaring buiten dat rapport, geen bulkdownload, maximaal 640 px). Patch van 15-09-2026 (zelfde versie): grondreferenties en `land_price_level` beperkt tot bouwgronden, projectgronden en gronden zonder nader subtype (landbouwgrond, weiden, bos, industriegrond en staanplaatsen via het subtype uitgesloten); `land_price_level` telt enkel zoekertjes tussen 15 en 5.000 EUR per m² grond; nieuwe `LandFeatures`-sleutel `land_type` (`building_plot` | `project_land` | `other`), het eerste woord van `summary` volgt die (additief). Additief: geen wijziging van paden of bestaande velden. Dienstversie 2.5.0. |
| 2.4.2 | 14-09-2026 | Actueel kadastraal percelenplan. De sectie `parcel` leest nu de laag PlanParcellaire van de FOD Financiën (lopende fiscale toestand, vandaag 01-01-2027) in plaats van de jaarlijkse INSPIRE-momentopname (01-01-2026); de INSPIRE-laag blijft de terugval bij een storing of een leeg antwoord. Nieuw veld `fiscal_situation` (ISO-datum van de fiscale toestand van het percelenplan, bv. `2027-01-01`; `2026-01-01` als de INSPIRE-terugval antwoordde; `null` als onbekend) op `parcel`, op elk item van `parcels[]` en op elk item van `parcel_candidates[]`. `source` vermeldt de laag (`CadGIS PlanParcellaire (FOD Financiën)` of `CadGIS INSPIRE (FOD Financiën)`). Additief: geen wijzigingen aan paden, parameters of bestaande velden. Dienstversie 2.4.2. |
| 2.4.1 | 08-09-2026 | Gebruik per API-sleutel: `/usage` krijgt het blok `per_key[]` (per sleutel: `prefix`, `label`, `environment`, `active`, `revoked_at`, `requests`, `cases`, `charged`, `sections`, `sections_delivered`, `amount`, `last_activity`; dezelfde filters als de rest van het antwoord; sleutels zonder aanvragen in de maand staan er niet in, ingetrokken sleutels met aanvragen wel). CSV: nieuwe laatste kolom `key_prefix` (16 kolommen). Geen wijzigingen aan paden, parameters of bestaande velden. Dienstversie 2.4.1 |
| 2.4.0 | 08-09-2026 | Basispakket: `basic` + `parcel` + `price_map` vormen nu één pakket per dossier; de AVM blijft een aparte optie. `billing.price` is nu `{package, avm, total, currency, excl_vat}`; in `/usage` gebruiken `total.sections`, `per_user[].sections` en `per_section` `package`/`avm`, `total.sections_delivered` toegevoegd; CSV-kolommen `price_package`, `price_avm`, `price_total`. `parcel` of `price_map` apart opvragen wordt genormaliseerd naar het pakket. Dienstversie 2.4.0 |
| 2.3.0 | 08-09-2026 | Sleutelbeheer door de partner (sectie 15): sleutels precies één keer getoond in het partnerdashboard op taxon.be (binnen 7 dagen, daarna wordt de versleutelde kopie vernietigd); eigen testsleutels (maximaal 3 actief, zelf intrekbaar); rotatie van de live-sleutel met 7 dagen overlap waarin antwoorden met de oude sleutel de melding `key_rotation_pending` dragen (ook in `/usage`), daarna `403 key_revoked`; de eerste live-sleutel komt nog altijd van Taxon na ondertekening; geen wijzigingen aan paden, parameters of velden; dienstversie 2.3.0 |
| 2.2.2 | 07-09-2026 | Fixes na de eerste externe integratietest (Propteo): fotolinks leveren altijd een serverthumbnail (standaard 640 px, nooit het origineel); `rental_value` altijd binnen `rental_value_range`; `parcel_candidates`, `parcel_candidates_source` en `parcel_candidates_remark` altijd aanwezig (`null` met `capakeys`); `parcel_candidates[].direction` en `garden_orientation` taalonafhankelijk (N/E/S/W); `zoning.category` is een Engelse enum; `source`-regels vertaald voor fr/en; één gemeentenaamregel voor de hele bundel (geen exoniemen); `new_build` enkel als `year_built` het niet tegenspreekt; Franse samenvattingen in het juiste geslacht; header- en parameterfouten in één 400; `free_reason: dedup` ook met een testsleutel als elke geleverde sectie al geleverd was; `pilot_status.expired` overal; blok `plan` in `/usage`; CSV-`Content-Type` met één charset; `Cache-Control` en `X-Content-Type-Options` één keer. Dienstversie 2.2.2. |
| 2.2.1 | 07-09-2026 | Adversariele QA-ronde (geen sleutel- of padwijzigingen): busnummers `box 3`, `b3`, `app 3` herkend; een adres zonder postcode en zonder gemeente geeft `422 address_imprecise`; `epc=A+` met een rauwe plus geeft 400 (stuur `A%2B`); de invoercache en de `Idempotency-Key`-vergelijking dekken alle parameters en de taal (andere invoer = verse bundel, nog steeds gratis voor al geleverde secties; zelfde sleutel met andere parameters = 409); `section_missing` voor een deelblok van basic draagt `section: basic` met de bloknaam in de message; `/usage` weigert een parameter die twee keer verstuurd wordt; price_map-geometrieen altijd geldig; gelijktijdige identieke aanvragen in pilot/test tellen een keer. Dienstversie 2.2.1. |
| 2.2.0 | 07-09-2026 | Elk referentiepunt draagt `features` (gestructureerde kenmerken van de advertentie, onbekend = `null`), `summary` (een zin, nl/fr/en), `thumbnail {url, valid_until}` (hoofdfoto 160 px, dezelfde foto als `photos[0]`) en `photo_count`; geen advertentietekst, geen prijswijziging; dienstversie 2.2.0 |
| 2.0.0 | 07-09-2026 | Engels contract: paden `/address`, `/usage`; header `X-User-Ref` (aliassen `X-Gebruiker-Ref`, `X-Kantoor-Ref` verouderd), `X-Case-Ref` (alias `X-Dossier-Ref`), `Accept-Language: en`; alle parameters, sleutels, enum-waarden en meldingscodes Engels; foutbody's met `charged`, `details[] {field, issue}`, scope `user_day`; nieuwe sectie `price_map` (dienst 2.1.0: meldingen `price_map_sparse`, `price_map_unavailable`, `upstream_errors.price_map: no_coverage`, een prijssleutel en een CSV-kolom voor de prijskaart, vervangen door de pakketsleutels in 2.4.0); `condition` bereikt nu de AVM; `/health` behoudt zijn sleutels, `versie` toont de dienstversie (2.1.0 op dat moment) |
| 1.2.0 | 07-09-2026 | Meerdere percelen per pand (`capakeys`, kandidaten, totalen), terugval grondoppervlakte, slaapkamerfilter, nieuwe 422-fouten en meldingen |
| 1.1.0 | 04-09-2026 | "kantoor" werd "gebruiker": gebruikersheader verplicht, kantoorheader verouderde alias; plafonds 20/300/60 vastgelegd; waakhond schorst de gebruiker |
| 1.0.0 | 03-09-2026 | Eerste contract: adresbundel, gebruik, health; dedup 30 dagen; Idempotency-Key 24 u; QA-ronde |

Mapping (oude 1.x-naam -> 2.0-naam). Oude namen werken niet meer, behalve de drie header-aliassen.

| Waar | 1.x | 2.0 |
|---|---|---|
| Paden | `/adres`, `/verbruik` | `/address`, `/usage` |
| Headers | `X-Gebruiker-Ref` (1.1), `X-Kantoor-Ref` (1.0), `X-Dossier-Ref` | `X-User-Ref`, `X-Case-Ref` (oude namen blijven aliassen) |
| 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` (grond 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` |
| Topniveau | `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` |
| comparables-blok | `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}`; nieuw in 2.5.0 (geen 1.x-equivalent): `low_sample` (grond) |
| comparable-item | `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}`; nieuw in 2.2.0 (geen 1.x-equivalent): `features {...}`, `summary`, `thumbnail {url, valid_until}`, `photo_count`; nieuw in 2.5.0: `price_per_m2_plot` (grond) |
| neighbourhood (was `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}`; nieuw in 2.5.0: `land_price_level {radius_m, count, price_per_m2_plot {p25, median, p75}, price_kind, max_age_months}` (grond) |
| amenities (was `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; ook in `parcels[]` en `parcel_candidates[]`, geen 1.x-equivalent), `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 (was `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 (was `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`; nieuw in 2.3.0 (geen 1.x-equivalent): `key_rotation_pending`; nieuw in 2.5.0: `avm_not_available_for_land`, `low_sample`, `housing_stats_not_available_for_land` |
| upstream_errors-sleutels | `basis`, `basis.comparables`, `basis.buurtstats`, `basis.voorzieningen`, `buurtstats.gebouwenpark`, `buurtstats.veiligheid`, `buurtstats.prijsniveau`, `buurtstats.epc_prijzen`; waarde `upstream_fout` | `basic`, `basic.comparables`, `basic.neighbourhood`, `basic.amenities`, `neighbourhood.building_stock`, `neighbourhood.safety`, `neighbourhood.price_level`, `neighbourhood.epc_prices`; waarde `upstream_error` |
| foutbody's | `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, interne naam `per_sleutel`, geen 1.x-equivalent), `per_day[] {day}`, `per_section {delivered, charged, amount}` (2.4.0: sleutels `package`, `avm`; `total.sections {package, avm}`, `total.sections_delivered`), `quality {error_pct, latency_p50_ms, latency_p95_ms, upstream_errors}`, `generated_at` |
| CSV-koptekst | `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, interne naam `sleutel_prefix`); bestand `taxon-usage-<client>-<month>.csv` |
## 15. Uw sleutels beheren

Sinds 08-09-2026 beheert u uw sleutels zelf in het partnerdashboard op taxon.be (https://taxon.be/api_partner, luik "Beheer"). Elke handeling daar wordt gelogd (wie, wanneer, vanaf welk IP-adres) en aan Taxon gemeld; de waarde van een sleutel staat nooit in een log of een e-mail.

| Handeling | Werking |
|---|---|
| Een sleutel eenmalig tonen | Een nieuwe sleutel wordt niet per e-mail verstuurd. Het dashboard toont ze precies één keer ("Sleutel eenmalig tonen"), binnen 7 dagen na de aanmaak; kopieer ze meteen naar uw secret store. Tot dat moment bewaart Taxon enkel een versleutelde kopie: na de eerste weergave, of na 7 dagen, wordt die kopie vernietigd en kan de sleutel niet meer getoond worden. Sleutels van vóór 08-09-2026 kunnen niet getoond worden; vraag een nieuwe aan of maak er zelf een. |
| Testsleutels | Maakt u zelf aan ("Testsleutel aanmaken", met een label), maximaal 3 actieve testsleutels; trekt u zelf in ("Intrekken": de sleutel werkt meteen niet meer, `401 invalid_api_key`). |
| Eerste live-sleutel | Wordt door Taxon uitgereikt na de ondertekende overeenkomst, klaar om één keer te tonen in uw dashboard. Een eerste live-sleutel maakt u niet zelf aan (`live_key_not_allowed`); het formulier "Live-sleutel aanvragen" in het dashboard mailt Taxon. |
| Een live-sleutel roteren | "Live-sleutel roteren" maakt een nieuwe live-sleutel aan (toon ze één keer, zet ze in productie). De vorige live-sleutel blijft 7 dagen werken; in die periode draagt elk antwoord van `/address` en `/usage` met de oude sleutel de melding `key_rotation_pending` (`section: null`, bericht met de einddatum). Na 7 dagen wordt de oude sleutel geweigerd (`403 key_revoked`, daarna `401 invalid_api_key`). Een live-sleutel trekt u nooit in vanuit het dashboard: roteer ze, of vraag Taxon ze onmiddellijk in te trekken. |
| Lek of vermoeden van misbruik | Roteer (live) of trek in (test) onmiddellijk en verwittig info@taxon.be wanneer de oude sleutel meteen moet vervallen in plaats van na 7 dagen. |

Foutcodes van de dashboardhandelingen (als tekst getoond in het dashboard, nooit op `/address` of `/usage`): `key_already_revealed`, `reveal_expired`, `not_revealable`, `key_limit_reached`, `live_key_not_allowed`, `not_self_revocable`.

Rotatieprocedure (aanbevolen): 1. roteer in het dashboard en toon de nieuwe sleutel één keer; 2. zet de nieuwe sleutel in uw secret store en deploy; 3. controleer dat uw antwoorden geen `key_rotation_pending` meer dragen; 4. de oude sleutel vervalt vanzelf na 7 dagen.
