Skip to content

REST API · plain HTTPS and JSON

One call. An answer you can act on.

Look up, validate and autocomplete Dutch and Belgian addresses against the official national registers. Every validation and lookup says what to do next, and every answer names the register and dataset version it came from.

2,500 verifications a month, free. Test keys included: 100 calls a day, never counted.

POST /v1/validate200
{
  "countryCode": "NL",
  "address1": "binenhof 1a",
  "zip": "2513aa",
  "city": "amsterdam",
  "lang": "en"
}

{
  "status": "corrected",
  "normalized": {
    "address1": "Binnenhof 1 A",
    "address2": null,
    "zip": "2513 AA",
    "city": "'s-Gravenhage",
    "countryCode": "NL"
  },
  "formatted": [
    "Binnenhof 1 A",
    "2513 AA 's-Gravenhage"
  ],
  "match": {
    "street": "Binnenhof",
    "houseNumber": 1,
    "houseNumberSuffix": "A",
    "unit": null,
    "unitType": null,
    "locality": "'s-Gravenhage",
    "postcode": "2513AA"
  },
  "suggestions": [],
  "confidence": "medium",
  "blocking": false,
  "issues": [
    {
      "code": "STREET_MISMATCH",
      "severity": "warning",
      "field": "address1",
      "message": "The street name doesn't match this postcode. Did you mean Binnenhof?"
    },
    {
      "code": "CITY_MISMATCH",
      "severity": "warning",
      "field": "city",
      "message": "The city doesn't match this postcode. Did you mean 's-Gravenhage?"
    }
  ],
  "carrier": {
    "street": "Binnenhof",
    "postcode": "2513 AA",
    "city": "'s-Gravenhage",
    "countryCode": "NL",
    "houseNumber": "1",
    "houseNumberAddition": "A",
    "warnings": []
  },
  "nextStep": "The address was corrected. Show the corrected address to the customer and use it if they agree.",
  "meta": {
    "datasetVersion": "nl-bag:2026-10-01+nl-lvbag:2026-10-08",
    "source": "Kadaster BAG",
    "attribution": null,
    "language": "en"
  }
}

Captured from the RightDoor API on 11 October 2026. Source: Kadaster BAG.

First call in minutes

Three steps to your first answer.

  1. Create a test key

    Sign up with email or GitHub and create a key in the dashboard. Test keys start with rd_test_, are free and allow 100 calls a day. Switch to a live key (rd_live_) when you ship.

  2. Send one request

    Plain HTTPS and JSON, with your key as a Bearer token in the Authorization header, never in the URL. No SDK to install: pick your language.

  3. Act on the status

    Four statuses, each with a nextStep. See what each one means.

curl

curl · nothing to install
curl https://api.rightdoor.eu/v1/validate \
  -H "Authorization: Bearer $RIGHTDOOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "countryCode": "NL",
    "address1": "binenhof 1a",
    "zip": "2513aa",
    "city": "amsterdam",
    "lang": "en"
  }'
# "status": "corrected"
# "meta": { "source": "Kadaster BAG", … }

Node.js

Node.js 18+ (ES module) · fetch is built in
const res = await fetch("https://api.rightdoor.eu/v1/validate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RIGHTDOOR_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    countryCode: "NL",
    address1: "binenhof 1a",
    zip: "2513aa",
    city: "amsterdam",
    lang: "en",
  }),
  signal: AbortSignal.timeout(5000), // 1.5 s if someone is waiting in a form
});
const result = await res.json();
// result.status: "corrected"
// result.formatted: ["Binnenhof 1 A", "2513 AA 's-Gravenhage"]

PHP

PHP · the curl extension
<?php
$ch = curl_init('https://api.rightdoor.eu/v1/validate');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT_MS => 5000, // 1500 if someone is waiting in a form
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RIGHTDOOR_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'countryCode' => 'NL',
        'address1' => 'binenhof 1a',
        'zip' => '2513aa',
        'city' => 'amsterdam',
        'lang' => 'en',
    ]),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
// On a timeout, a network error or a 5xx, accept the address as typed (fail open).
$result = ($body === false || $code >= 500) ? null : json_decode($body, true);
// $result['status']: "corrected"

Rust

Rust · reqwest (json), tokio, serde_json
use serde_json::{json, Value};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let key = std::env::var("RIGHTDOOR_KEY")?;
    let result: Value = reqwest::Client::new()
        .post("https://api.rightdoor.eu/v1/validate")
        .bearer_auth(key)
        .timeout(Duration::from_secs(5))
        .json(&json!({
            "countryCode": "NL",
            "address1": "binenhof 1a",
            "zip": "2513aa",
            "city": "amsterdam",
            "lang": "en"
        }))
        .send()
        .await?
        .json()
        .await?;
    println!("{} {}", result["status"], result["formatted"]); // "corrected" ["Binnenhof 1 A","2513 AA 's-Gravenhage"]
    Ok(())
}

No RightDoor package to install: these are plain HTTPS calls. Generate a typed client from the OpenAPI spec if you want one.

Three endpoints

Find it, check it, finish it.

POST /v1/validate

Validate

A whole address in, a verdict out: valid, corrected, ambiguous or invalid. With the register's version as normalized fields and ready-to-print formatted lines, issue codes, carrier fields and a nextStep.

NL · BEcountryCode picks the register
GET /v1/{country}/lookup

Lookup

Netherlands: postcode and house number. Belgium: postcode, street and number. Every address at that number comes back, with the additions (toevoeging, bus) to pick from. Also as /v1/nl/lookup and /v1/be/lookup.

<1 msRegister read for a Dutch postcode + number, p99 (the read alone, without HTTP)
GET /v1/autocomplete

Autocomplete

Type-ahead for the whole address: a street and number, a postcode and number, or a place. Up to 10 suggestions. When search doesn't answer in time you get degraded: true and fewer suggestions, never an error.

~10 msFull-address search (NL), median, one query at a time · under 20 ms at p99

Server-side timings, measured 9–10 October 2026 on the server type our search nodes run on (4 vCPU, NL-AMS1), one query at a time with a warm cache. The lookup figure is the register read and the search figure the search-engine query, both without HTTP; your network round trip comes on top. Under load or from a cold cache, search p99 is around 45 ms.

Also GET /v1/suggest for street names while someone types. Suggestions, lookups and autocomplete for one address count as one verification when they share a sessionId with the validation that ends it; without one, 10 suggestion calls count as one. Every parameter and response: API reference.

Statuses

Every verdict comes with a next step.

The four statuses
StatusWhat it meansWhat your form does
validExactly one address matches, and what was typed already matches it (letter case aside). New builds match like any other address, with an ADDRESS_PLANNED note.Accept it. Use normalized or formatted.
correctedExactly one address matches, but something was typed differently: a typo in the street, the wrong city, a postcode without its space.Show the register's version. Let the customer keep theirs.
ambiguousSeveral addresses fit, usually a number with additions or bus numbers and none given. Up to 10 of them are in suggestions; a lookup lists every address at the number.Ask which one, from suggestions.
invalidNo match in the register.If blocking is true, ask the customer to check the field the issue names. If it's false, let them confirm and accept it.
blocking
Advice, computed conservatively: true only when the address certainly can't be delivered as written. No house number, a missing or impossible postcode, a PO box when you haven't set allowPoBox, or a Dutch postcode where neither the number nor the street exists. With customerConfirmed: true, only format errors (including a missing house number) and PO boxes still block. You decide what your form does with it.
confidence
high: one exact match. medium: one match after a correction. low: several possible addresses, a partial match, a number the register doesn't have, or an unclear country.
nextStep
One plain sentence saying what to do, in the language you ask for with lang (nl, fr, de or en).
issues
Machine-readable codes such as STREET_MISMATCH, each with a short message and the field it's about.

Fail open. Give RightDoor 1.5 s when someone is waiting in a form and 5 s elsewhere. On a timeout, a 5xx or a network error, accept the address as typed and check it again later. RightDoor never needs to block a real customer.

Data you can trace

Official registers, versioned.

Each country's official national address register. meta.source names it and meta.datasetVersion the dataset version on every answer; meta.attribution carries the credit line where the licence asks for one. Hosted in Amsterdam; the addresses you send are never stored.

See every source
  • NL BAG: Netherlands, live
  • BE BeST Address: Belgium, live

Your first call is free.