# Places API
> Address autocomplete and structured lookup API for the Netherlands, Belgium and Luxembourg.
Base URL: `https://places-api.com/api/v1`
Full OpenAPI spec: [/openapi.yaml](https://places-api.com/openapi.yaml)
Interactive docs: [/docs.html](https://places-api.com/docs.html)
## Getting started
1. Create an account at [places-api.com/users/register](https://places-api.com/users/register)
2. Generate an API key at [places-api.com/users/api-keys](https://places-api.com/users/api-keys)
3. Pass the key in the `X-API-Key` header with every request
## Authentication
All requests require an API key in the `X-API-Key` header.
Two key types:
- **Secret keys** (`sk_` prefix) — server-side use, no domain validation
- **Publishable keys** (`pk_` prefix) — client-side/browser use, validates `Origin`/`Referer` against the key's allowed domains (supports wildcards like `*.example.com` and `localhost`)
## GET /autocomplete
Search for addresses by query string.
### Parameters
| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `q` | string | yes | — | Search query, minimum 2 characters |
| `country` | string | no | all | Comma-separated ISO codes: `NL`, `BE`, `LU` |
| `limit` | integer | no | 10 | Max results, 1–50 |
### Response
```json
{
"results": [
{
"display": "Kerkstraat 1, 1234AB Amsterdam",
"street": "Kerkstraat",
"housenumber": "1",
"housenumber_addition": null,
"postcode": "1234AB",
"city": "Amsterdam",
"country": "NL"
}
],
"count": 1
}
```
**Fields:**
- `display` (string) — formatted full address
- `street` (string) — street name (language-matched for Belgian addresses)
- `housenumber` (string) — house number
- `housenumber_addition` (string|null) — suffix like A, B, bis
- `postcode` (string) — postal code
- `city` (string) — city name (language-matched for Belgian addresses)
- `country` (string) — `NL`, `BE`, or `LU`
### Errors
| Status | Error | When |
|--------|-------|------|
| 400 | `Missing required parameter: q` | `q` param absent |
| 400 | `Query must be at least 2 characters` | `q` too short |
| 401 | `Missing or invalid API key` | bad or missing `X-API-Key` |
| 403 | `Domain not allowed for this key` | publishable key + wrong origin |
| 422 | `Invalid country code(s): XX. Valid codes: NL, BE, LU` | invalid `country` value |
| 429 | `Rate limit exceeded` | burst or monthly limit hit |
On 429, response headers include: `ratelimit-limit`, `ratelimit-remaining`, `ratelimit-reset` (seconds).
## GET /lookup
Structured address lookup by postcode and house number. Returns exact matches, not fuzzy search results.
### Parameters
| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `postcode` | string | yes | — | Postal code (e.g., `1234AB`, `1000`). NL codes are auto-normalized. |
| `housenumber` | string | no | — | House number (e.g., `42`). Omit for postcode-level info. |
| `country` | string | yes | — | ISO code: `NL`, `BE`, or `LU` |
### Response
**Single match** (postcode + housenumber → one address):
```json
{
"status": "found",
"address": {
"street": "Kerkstraat",
"housenumber": "1",
"housenumber_addition": null,
"postcode": "1234AB",
"city": "Amsterdam",
"country": "NL"
}
}
```
**Multiple additions** (e.g., 42A, 42B exist):
```json
{
"status": "found",
"address": { "street": "Keizersgracht", "housenumber": "42", "housenumber_addition": null, "postcode": "1015CS", "city": "Amsterdam", "country": "NL" },
"additions": ["A", "B", "I", "II", "hs"]
}
```
**Postcode only (NL)** — returns street + house number range:
```json
{
"status": "found",
"street": "Meidoornlaan",
"city": "Roosendaal",
"country": "NL",
"housenumber_range": { "min": 1, "max": 89 }
}
```
**Postcode only (BE/LU)** — returns city only (postcodes cover entire municipalities):
```json
{
"status": "found",
"city": "Brussel",
"country": "BE"
}
```
**Not found:**
```json
{
"status": "not_found"
}
```
### Errors
| Status | Error | When |
|--------|-------|------|
| 400 | `Missing required parameter: postcode` | `postcode` absent |
| 400 | `Missing required parameter: country` | `country` absent |
| 401 | `Missing or invalid API key` | bad or missing `X-API-Key` |
| 403 | `Domain not allowed for this key` | publishable key + wrong origin |
| 422 | `Invalid country code: XX. Valid codes: NL, BE, LU` | invalid `country` value |
| 429 | `Rate limit exceeded` | burst or monthly limit hit |
## Examples
```bash
# Basic search
curl -H "X-API-Key: YOUR_API_KEY" \
"https://places-api.com/api/v1/autocomplete?q=kerkstraat%201"
# Filter by country
curl -H "X-API-Key: YOUR_API_KEY" \
"https://places-api.com/api/v1/autocomplete?q=rue%20de%20la%20loi&country=BE"
# Multiple countries, limited results
curl -H "X-API-Key: YOUR_API_KEY" \
"https://places-api.com/api/v1/autocomplete?q=avenue&country=BE,LU&limit=5"
```
## Autocomplete Widget
A drop-in JavaScript widget is available at `/places-autocomplete.js`. No build step required.
### Method 1: Data Attributes (zero JS)
```html
```
**Data attributes:**
| Attribute | Required | Default | Description |
|-----------|----------|---------|-------------|
| `data-places-autocomplete` | yes | — | Enables auto-init on the input |
| `data-places-api-key` | yes | — | Your API key |
| `data-places-country` | no | all | Comma-separated: `NL`, `BE`, `LU` |
| `data-places-limit` | no | 10 | Max results |
| `data-places-debounce` | no | 200 | Debounce delay in ms |
| `data-places-min-length` | no | 2 | Min chars before searching |
| `data-places-url` | no | `https://places-api.com/api/v1` | API base URL override |
### Method 2: JavaScript API
```javascript
const input = document.getElementById("address");
PlacesAutocomplete.init(input, {
apiKey: "pk_live_xxx",
country: "NL,BE,LU",
limit: 5
});
input.addEventListener("places:select", (e) => {
console.log(e.detail);
// e.detail contains: { display, street, housenumber,
// housenumber_addition, postcode, city, country }
});
```
**Options:** `apiKey`, `country`, `limit`, `debounce` (ms, default 200), `minLength` (default 2), `url`.
**Methods:**
- `PlacesAutocomplete.init(element, options)` — initialize on an input, returns instance
- `PlacesAutocomplete.destroy(element)` — remove widget and clean up
**Events:**
- `places:select` — fired on the input when an address is selected. `event.detail` contains the address object (same shape as the API response).
### Keyboard support
Arrow Up/Down to navigate, Enter to select, Escape to close. Full ARIA combobox pattern.
## Lookup Widget Mode
The widget also supports a `lookup` mode for the standard Dutch postcode + house number pattern.
### Data Attributes
```html
```
### JavaScript API
```javascript
PlacesAutocomplete.init(postcodeInput, {
apiKey: "pk_live_xxx",
country: "NL",
mode: "lookup",
fields: {
housenumber: "#housenumber",
addition: "#addition-select",
street: "#street",
city: "#city"
}
});
postcodeInput.addEventListener("places:lookup", (e) => {
console.log(e.detail); // Full lookup API response
});
```
**Lookup mode fields:** `housenumber` (watched), `addition` (populated as `