POST /v1/validateValidate
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.
REST API · plain HTTPS and JSON
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.
{
"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
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.
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.
Four statuses, each with a nextStep. See what each one means.
curl
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
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
$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
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
POST /v1/validateA 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.
GET /v1/{country}/lookupNetherlands: 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.
GET /v1/autocompleteType-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.
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
| Status | What it means | What your form does |
|---|---|---|
| valid | Exactly 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. |
| corrected | Exactly 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. |
| ambiguous | Several 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. |
| invalid | No 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. |
blockingallowPoBox, 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.confidencenextSteplang (nl, fr, de or en).issuesSTREET_MISMATCH, each with a short message and the field it's about.Data you can trace
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.
Or browse the API reference and the OpenAPI spec.