# WoningMe Developer API reference
Programmatic access to rental listings across the Netherlands, covering both
**private-sector** (vrije sector) and **housing-association / public-sector**
(sociale huur, middenhuur) rentals, scoped to search filters you define, with
optional webhook notifications when new listings match.

Same underlying listings data that powers the [WoningMe app](https://woning.me).

## Getting started

1. [Create a developer account](/developer/register). The same account works for the WoningMe app.
2. Open your [dashboard](/developer/dashboard) and click "Generate API key". Copy it immediately, it's shown once.
3. Call `GET /api/developer/v1/listings?city=Utrecht&max_price=1500` for listings right away (no setup), or create a search filter and set [one webhook URL](#webhooks) to be told about every new match.

Building with an AI assistant? Copy the prompt at https://woning.me/developer/build-with-ai, or connect the MCP server at https://woning.me/mcp (https://woning.me/developer/mcp-server).

The API works once billing is set up. New accounts start with a **2-day trial**: add a
payment method in your dashboard, use the API right away, and the first charge of
**EUR 15** comes after the two days unless you cancel. Cancel during the trial and
nothing is charged. Without billing, requests return `402`.

## Using the API efficiently

- For new listings, use a webhook instead of polling (PUT /webhook after creating saved searches).
- If you must poll, do it at most once every 5 minutes per query, and ask only for what is new: set listed_since to the time of your last check (ISO date or datetime) with sort=first_seen_at, order=desc.
- Be specific. Always set city and the filters you care about (max_price, min_rooms, property_type), and keep limit small. Do not page through all results: use total from the first response to know how many exist, and stop when you have enough.
- Never repeat an identical query within a minute: answers are cached for 60 seconds, and a response with a notice field means you are repeating yourself.
- Keep your own copy. Store results and reuse them instead of asking again for the same listings.
- Send requests one after another, not in parallel bursts, and prefer one well-filtered request over many near-identical ones.
- Watch the X-RateLimit-Remaining header. On HTTP 429 wait for the Retry-After seconds and then back off (double the wait on each repeated 429).
- Page with cursor, not skip. Do not create a saved search for every question: saved searches (max 10) exist for webhooks. Do not regenerate your key or send test events in a loop.

## Listing details (planned)

Listing details (all images, the full description) are not available through the API yet. When a details endpoint is added, these rules apply:

- Request the images of a listing only when a user is viewing that listing's detail page, one listing per user action.
- Request the description of a listing only when a user is viewing that listing's detail page, one listing per user action.
- Do not fetch details periodically, in a loop, in bulk, or in advance for a list of listings, and do not poll or refresh them on a schedule. This is not allowed.
- Until then: show the main image (imageUrl) and link to the original listing (url) for everything else.

## Authentication

Every request must include your API key in the `X-Api-Key` header. Click **Authorize**
above to set it once for every request you try on this page.

Requests are rate limited to **30 per minute** per API key. Exceeding this returns `429`.

## Limits

Five general safeguards apply to keep the API reliable for everyone:

- A search filter's `radius_km` may not exceed **50 km**. Creating or updating a search
  filter with a larger radius returns `422`.
- An account may have at most **10** search filters. Creating one more returns `403`.
- The [Rental properties](#properties) endpoint returns at most **50** properties per
  request by default (pass `limit` to request fewer; requesting more than 50 returns
  `422`).
- The [Rental properties](#properties) endpoint may be called at most **5,000** times per
  rolling 24 hours and **50,000** times per rolling 30 days, per API key. Exceeding either
  returns `429`.
- Creating, updating, or deleting a search filter is a "mutation": at most **20** per
  rolling minute and **50** per rolling 24 hours, per API key. Exceeding either returns
  `429`.

These are general safeguards against misuse, not restrictions on legitimate use. Get in
touch if your use case needs higher limits.

## Webhooks

Set **one webhook URL for your whole account** (`PUT /api/developer/v1/webhook`). We send an
HTTP `POST` to it for every new rental property that matches any of your search filters.
A listing that matches several of your filters is sent **once**, and `searches` lists every
filter it matched, so you always know which filter(s) it belongs to:

```
POST https://your-app.com/hooks/woningme
Content-Type: application/json
X-WoningMe-Event: listing.new
X-WoningMe-Delivery: 1234
X-WoningMe-Signature: sha256=<hex digest>

{
  "event": "listing.new",
  "searches": [
    {"id": 42, "label": "Amsterdam centrum"},
    {"id": 57, "label": "Utrecht < 1500"}
  ],
  "property": { /* same shape as the Rental properties endpoint below */ }
}
```

Use the Account webhook endpoints to set the URL, read or rotate the signing secret, send a
test event (`webhook.test`) and list recent deliveries. All search filters of your account
count, including ones you create from the dashboard.

**Per-filter webhook (optional).** A search filter can still have its own `webhook_url`. Such
a filter is delivered to that URL only, in the older one-filter format
(`{"event": "listing.new", "searchId": 42, "search": {...}, "property": {...}}`, signed with
that filter's own `webhookSecret`), and is left out of the account webhook messages. Existing
integrations keep working unchanged.

**Verifying the signature.** `X-WoningMe-Signature` is an HMAC-SHA256 digest of the raw
request body, using your webhook's `secret` (returned by the Account webhook endpoints; for
a per-filter webhook, that filter's `webhookSecret`) as the key. Always verify it before
trusting a payload.

```python
# Python
import hashlib, hmac

def verify(request_body: bytes, signature_header: str, webhook_secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        webhook_secret.encode(), request_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
```

```javascript
// Node.js
const crypto = require("crypto");

function verify(rawBody, signatureHeader, webhookSecret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", webhookSecret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
```

**Retries.** If your endpoint doesn't respond with a `2xx` status (or times out), we
retry once after about 30 seconds, then give up. Check the Deliveries endpoint to see
what happened (the account webhook has its own Deliveries endpoint). Webhook delivery stops automatically when your subscription ends or
payment fails, independent of any WoningMe Plus subscription on your account.

## Errors

Errors follow the shape `{"detail": "..."}`.

| Status | Meaning |
|---|---|
| `401` | Missing or invalid API key. |
| `402` | No active subscription. Set up billing in the dashboard (new accounts get a 2-day trial). |
| `403` | Maximum of 10 search filters per account reached. |
| `404` | The search filter doesn't exist. |
| `422` | Request body failed validation (missing/invalid field), a search filter's radius exceeds 50 km, or a `limit` over your account's maximum properties per request. |
| `429` | Rate limit exceeded: 30 requests per minute overall; for [Rental properties](#properties), also 5,000 per 24 hours / 50,000 per 30 days; for search-filter mutations, also 20 per minute / 50 per 24 hours. All per API key. |

Full machine-readable spec: https://woning.me/developer/openapi.json
Interactive HTML version: https://woning.me/developer/docs
