WoningMeDevelopers
Stored only in this browser. Never sent anywhere except the requests you send below.

API reference

REST API for Dutch rental listings, with signed webhooks for new matches. Base URL: https://woning.me/api/developer/v1

Getting started

The WoningMe Developer API gives you programmatic access to rental listings across the Netherlands, both private-sector and housing-association/public-sector, scoped to search filters you define, with optional webhook notifications when new listings match.

  1. Create a developer account. The same account works for the WoningMe app.
  2. Open your dashboard and click "Generate API key". Copy it immediately, it's shown once.
  3. Call GET /listings for listings right away, with no setup, or create a search filter and set one webhook URL to be told about every new match.

Building with an AI assistant? , read Build with AI, or connect the MCP server.

A machine-readable OpenAPI spec is also available, for client generators, Postman/Insomnia import, and similar tooling.

The API works as soon as billing is set up. New accounts get a 2-day trial: add a payment method in the dashboard, use everything right away, and the first charge of €15 follows after two days unless you cancel. Cancel during the trial and nothing is charged. Until billing is set up, requests return 402.

Authentication

Every request must include your API key in the X-Api-Key header:

curl https://woning.me/api/developer/v1/searches \
  -H "X-Api-Key: YOUR_API_KEY"

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 endpoint returns at most 50 properties per request by default (pass limit to request fewer; requesting more than 50 returns 422).
  • The Rental 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.

Using the API efficiently

The API is a shared service. Following these rules keeps it fast for everyone and keeps you well inside your limits. They apply to scripts, apps and AI assistants alike.

  • 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.

Every response from an API-key route carries X-RateLimit-Limit and X-RateLimit-Remaining (requests left in the current minute). Errors have a hint that says how to fix them, and a rate-limit error has Retry-After.

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.

This endpoint does not exist yet. The rules are published in advance so you and your AI assistant can build with them from the start.

Search listings

GET /api/developer/v1/listings

The quickest way to get listings: one call, no saved search needed. Combine any of the filters below, sort, and page with a cursor. Use search filters when you want a webhook for new matches.

Query paramDefaultNotes
citynoneDutch city. Forgiving: case, accents, aliases (den haag) and small typos. The response's resolvedCity shows the match. Without radius_km it matches the city name.
radius_kmnoneRadius around city, up to 50 km.
min_price, max_pricenoneWhole euros per month.
min_rooms, min_surfacenoneMinimum rooms, minimum m².
property_type, sourcenoneFor example apartment, house, studio, room; or one source.
is_free_market, is_social_housing, is_middenhuurallSegment flags.
listed_sincenoneISO date: only listings first seen on or after it.
sort, orderfirst_seen_at, descNewest first by default. Sort fields as for Rental properties.
limit, cursor50Max 50 per request; pass nextCursor back as cursor.
fuzzytruefalse uses the city exactly as typed.
curl "https://woning.me/api/developer/v1/listings?city=utrecht&max_price=1500&property_type=apartment&limit=20" \
  -H "X-Api-Key: YOUR_API_KEY"

The response has the same shape as Rental properties plus resolvedCity. It counts towards the same daily and monthly property-request limits.

Try it

Search filters

A search filter defines a city, radius, housing segments, and optional price/rooms/surface minimums/maximums. You can't query rental properties directly: every property lookup and webhook is scoped to a search filter you've created first. The endpoint path is /searches; "search filter" is just what we call it in these docs.

Creates a new search filter. webhook_url is optional and normally not needed: new listings for all your search filters are sent to your account webhook. Set it only if this one filter must go to its own URL.

By default a new search filter matches all three housing segments (private-sector, housing-association, and mid-market rent). Set is_free_market / is_social_housing / is_middenhuur explicitly to narrow it to just the segment(s) you want, for example is_social_housing: true alone returns only housing-association listings.

FieldTypeRequiredNotes
labelstringyesA name you choose for this search filter.
citystringyesDutch city or municipality name.
radius_kmnumberyesSearch radius in km from the city center. Max 50 km, see Limits.
is_free_marketbooleannoInclude private-sector (vrije sector) listings. Segment flags are independent; see note above.
is_social_housingbooleannoInclude housing-association / public-sector (corporatie, sociale huur) listings.
is_middenhuurbooleannoInclude mid-market rent (middenhuur) listings.
min_priceintegernoMinimum monthly rent in whole euros. Omit for no minimum.
max_priceintegernoMaximum monthly rent in whole euros. Omit for no maximum.
min_roomsintegernoMinimum number of rooms. Omit for no minimum.
min_surfaceintegernoMinimum floor area in square meters. Omit for no minimum.
webhook_urlstringnoOptional own webhook URL for this filter only (replaces the account webhook for it). Omit to use your account webhook.
curl -X POST https://woning.me/api/developer/v1/searches \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "Amsterdam centrum", "city": "Amsterdam", "radius_km": 5.0, "is_social_housing": true, "min_price": 700, "webhook_url": "https://your-app.com/hooks/amsterdam"}'

201 Created, with a Location header pointing at the new search filter:

{
  "id": 42,
  "label": "Amsterdam centrum",
  "city": "Amsterdam",
  "radiusKm": 5.0,
  "createdAt": "2026-07-27T20:00:00+00:00",
  "propertiesUrl": "/api/developer/v1/searches/42/properties",
  "isFreeMarket": false,
  "isSocialHousing": true,
  "isMiddenhuur": false,
  "minPrice": 700,
  "maxPrice": null,
  "minRooms": null,
  "minSurface": null,
  "webhookUrl": "https://your-app.com/hooks/amsterdam",
  "webhookSecret": "a3f9c2e1..."
}

maxPrice, minRooms, and minSurface are null when no filter was set for that field. webhookSecret is generated the first time you set a webhook_url and stays the same across later changes to the URL. See Webhooks for how to use it.

GET /api/developer/v1/searches

Lists all of your search filters.

curl https://woning.me/api/developer/v1/searches \
  -H "X-Api-Key: YOUR_API_KEY"
{
  "searches": [
    { "id": 42, "label": "Amsterdam centrum", "city": "Amsterdam", "radiusKm": 5.0, ... }
  ]
}
Try it

Fetches a single search filter by ID. Returns 404 if it doesn't exist.

curl https://woning.me/api/developer/v1/searches/42 \
  -H "X-Api-Key: YOUR_API_KEY"

Updates a search filter. Only fields you include are changed. Omit a field to leave it as-is. Send an empty string for webhook_url to remove the filter's own webhook (this also clears its secret); the filter then uses your account webhook again.

There is currently no way to clear a price/rooms/surface filter back to unset via PATCH once set; delete and recreate the search filter instead.

curl -X PATCH https://woning.me/api/developer/v1/searches/42 \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"radius_km": 7.5}'

Deletes a search filter. Returns 204 No Content.

curl -X DELETE https://woning.me/api/developer/v1/searches/42 \
  -H "X-Api-Key: YOUR_API_KEY"

Rental properties

GET /api/developer/v1/searches/{id}/properties

Returns rental properties matching that search filter's city, radius, and segments, further narrowed by any of the query params below. All query params are optional and combine with each other and with the search filter, for example ?source=nederwoon&property_type=apartment&max_price=1500 returns only Nederwoon apartments at or under €1,500. The geographic and segment scope always comes from the search filter and can't be widened per request; min_price/max_price/min_rooms/min_surface can only narrow the search filter's own values further, not loosen them.

Query paramDefaultNotes
sourcenonefilter to one source, e.g. nederwoon
property_typenonefilter to one type, e.g. apartment, house, studio, room
min_pricenonewhole euros; combines with the search filter's own min_price, the stricter of the two wins
max_pricenonewhole euros; combines with the search filter's own max_price, the stricter of the two wins
min_roomsnonecombines with the search filter's own min_rooms, the stricter of the two wins
min_surfacenonem²; combines with the search filter's own min_surface, the stricter of the two wins
sortpriceprice, surface, rooms, available, city, last_synced_at, first_seen_at
orderascasc or desc
limityour account's max (50 for standard developer accounts)max 1000
cursornoneopaque cursor from the previous response's nextCursor; omit for the first page

Results are paginated with cursor: the first request omits it, and each response's nextCursor is passed back as the cursor query param to fetch the next page. nextCursor is null once there are no more results. This stays correct even if listings are added or removed between requests, which offset-based pagination (a fixed skip count) can't guarantee.

curl "https://woning.me/api/developer/v1/searches/42/properties?source=nederwoon&property_type=apartment&max_price=1500" \
  -H "X-Api-Key: YOUR_API_KEY"
{
  "total": 274,
  "limit": 2,
  "properties": [
    {
      "id": 19022,
      "source": "nederwoon",
      "sourceLabel": "Nederwoon",
      "url": "https://...",
      "imageUrl": "https://...",
      "street": "Kerkstraat 12",
      "postcode": "1234AB",
      "city": "Amsterdam",
      "propertyType": "Apartment",
      "constructionType": null,
      "surfaceM2": 75,
      "rooms": 3,
      "availableFrom": null,
      "priceEurCents": 145000,
      "priceText": "€ 1.450",
      "reactions": null,
      "agent": "Some Agency",
      "agentWebsite": "https://...",
      "agentPhone": null,
      "socialHousing": false,
      "isHousingAssociation": false,
      "isMiddenhuur": false,
      "assignmentType": null,
      "neighbourhoodScore": { "score": 6.8, "class": "ruim voldoende", "year": 2024, "dimensions": {...} },
      "lat": 52.37,
      "lon": 4.90,
      "firstSeenAt": "2026-07-27T20:20:00+00:00",
      "lastSyncedAt": "2026-07-27T20:20:00+00:00"
    }
  ],
  "nextCursor": "eyJ2IjoxNDUwMDAsImlkIjoxOTAyMn0="
}

Many fields are nullable, since sources don't all publish the same data: constructionType, availableFrom, reactions, agentWebsite, agentPhone, and assignmentType are frequently null; imageUrl, street, postcode, surfaceM2, rooms, priceEurCents, agent, lat, and lon are null when the source didn't publish that field or a listing couldn't be geocoded; neighbourhoodScore is null when unavailable for that postcode.

Try it

Webhooks

Set one webhook URL for your whole account and we send an HTTP POST to it for every new listing that matches any of your search filters. No polling required, and no URL per filter: each message tells you which filter(s) it belongs to.

Payload

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 above */ }
}

A listing that matches several of your filters is sent once; searches lists every filter it matched (use id to route it in your own system, label is the name you gave the filter). X-WoningMe-Delivery is stable across the retry, so you can use it to ignore duplicates.

Set up your webhook

EndpointPurpose
PUT /api/developer/v1/webhookSet or change the URL: {"url": "https://your-app.com/hooks/woningme"}. The response contains your signing secret, created the first time and kept when you change the URL.
GET /api/developer/v1/webhookRead the URL and secret (404 if none is set).
POST /api/developer/v1/webhook/testSend a signed webhook.test event to your URL and see whether it answered with a 2xx.
POST /api/developer/v1/webhook/rotate-secretCreate a new signing secret. Deliveries use it immediately.
GET /api/developer/v1/webhook/deliveriesRecent deliveries, newest first, each with the searchIds it matched.
DELETE /api/developer/v1/webhookRemove the webhook.
curl -X PUT https://woning.me/api/developer/v1/webhook \
  -H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.com/hooks/woningme"}'

You can also manage all of this in your dashboard. All search filters of your account count, including ones created from the dashboard.

Own webhook for a single filter (optional)

A search filter can still have its own webhook_url. That filter is then delivered only to its own URL, in the older one-filter format ({"event": "listing.new", "searchId": 42, "search": {"id": 42, "label": "..."}, "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

The X-WoningMe-Signature header is an HMAC-SHA256 digest of the raw request body, using your webhook's secret as the key (for a filter's own webhook: that filter's webhookSecret). Always verify it before trusting a payload.

# 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)
// 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 delivery history to see what happened (the account webhook has its own /webhook/deliveries endpoint).

Webhook delivery stops automatically when your subscription ends or a payment fails (see Getting started). This is independent of any WoningMe Plus subscription on your account.

MCP server

Let an AI assistant use the API directly. The MCP server at https://woning.me/mcp (Streamable HTTP) offers tools to search rentals, manage saved searches and set the webhook. It uses your API key (X-Api-Key or Authorization: Bearer) and the same billing and limits as the REST API.

claude mcp add --transport http woningme https://woning.me/mcp \
  --header "X-Api-Key: YOUR_API_KEY"

Setup for Cursor and other clients, the list of tools and examples: MCP server guide.

Deliveries

GET /api/developer/v1/searches/{id}/deliveries

Returns the recent webhook delivery attempts for a search filter. Useful for debugging your endpoint.

curl https://woning.me/api/developer/v1/searches/42/deliveries \
  -H "X-Api-Key: YOUR_API_KEY"
{
  "deliveries": [
    {
      "id": 501,
      "propertyId": 19022,
      "status": "success",
      "httpStatus": 200,
      "error": null,
      "attemptCount": 1,
      "attemptedAt": "2026-07-27T20:20:49+00:00"
    }
  ]
}

status is one of success, pending_retry, or failed.

Try it

Errors

StatusMeaning
401Missing or invalid API key.
402No active subscription. Set up billing in the dashboard; new accounts get a 2-day trial.
403Maximum of 10 search filters per account reached.
404The search filter doesn't exist.
422Request 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.
429Rate limit exceeded: 30 requests per minute overall; for Rental 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.

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