Skip to content

Open Beta – help us test! All listings are examples only.

Build a discovery aggregator

Aggregate public listings across organizations with cursor pagination and ETag caching, and monitor your own API usage with the usage endpoint.

This guide shows how to build a cross-organization listings aggregator feed — a portal or comparison site that pulls public listings discreetly and efficiently — and how to keep an eye on your own consumption with the usage endpoint.

The problem this solves: you run an aggregator and need a clean, paginated, cacheable feed of public listings that respects discreetly-marketed properties, plus a way to watch your monthly quota.

Beta / dark-shipped. Discovery (Plan 60) is gated behind DISCOVERY_API_ENABLED and Usage (Plan 61) behind USAGE_API_ENABLED; while off they return 404. Both operations are x-internal and not yet in the public reference. Usage is GA-ready and may be enabled independently. Ask your WOHNO contact to enable the flags you need.

Prerequisites

  • For discovery: a publishable key (pk_live_…) with an origin allowlist — discovery:read is on the publishable whitelist — or a secret key.
  • For usage monitoring: a secret key (sk_live_…) with usage:read. Usage is sk-only (business figures must not sit in a browser key); a pk_ key is rejected with 403 INSUFFICIENT_SCOPE.
  • The DISCOVERY_API_ENABLED and/or USAGE_API_ENABLED flags enabled.

Step 1 — Page through the discovery feed

Discovery returns a reduced DiscoveryListingDto. Discreetly-marketed, inactive and blocked listings are structurally excluded, geo coordinates are rounded to ~110 m, and street/house-number are never returned.

curl "https://wohno.de/api/v1/discovery/listings?city=Berlin&rooms_min=2&limit=50" \
  -H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Origin: https://aggregator.example.com"

Response (200, cursor-paginated):

{
  "data": [
    {
      "id": "9f1c2a3b-...",
      "title": "Bright 2-room apartment",
      "city": "Berlin",
      "zip": "10115",
      "rooms": 2,
      "rent_max": 980,
      "property_type": "apartment",
      "lat": 52.53,
      "lng": 13.38
    }
  ],
  "pagination": { "next_cursor": "eyJpZCI6...", "has_more": true, "limit": 50 }
}

Available filters: city, zip (1–5 digits), rooms_min, rent_max, property_type. limit is 1–50. Keep passing pagination.next_cursor as the next ?cursor= while has_more is true.

Step 2 — Cache with ETags

Each response carries an ETag. Store it and send it back as If-None-Match to get a cheap 304 Not Modified when nothing changed — this saves bandwidth and counts more favorably against your quota:

curl "https://wohno.de/api/v1/discovery/listings?city=Berlin&limit=50" \
  -H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Origin: https://aggregator.example.com" \
  -H 'If-None-Match: W/"l3-1a2b3c4d5e6f7a8b"' \
  -i

A 304 comes back with no body — reuse your cached page.

Step 3 — Respect discreet listings

You do not need to do anything special: discreetly-marketed, inactive and blocked listings never appear in this feed (enforced by the public_discovery_listings view). Do not try to cross-reference IDs with other endpoints to re-expose them.

Step 4 — Monitor your usage

Pull your own quota/consumption for the current month with a secret key:

curl "https://wohno.de/api/v1/usage" \
  -H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Response (200):

{
  "data": {
    "plan": "plus",
    "api_requests": {
      "used": 3450,
      "limit": 10000,
      "reset_at": "2026-07-01T00:00:00.000Z"
    },
    "webhook_deliveries": {
      "used": 120,
      "limit": 5000,
      "reset_at": "2026-07-01T00:00:00.000Z"
    },
    "embed_impressions": {
      "used": 8800,
      "limit": 50000,
      "reset_at": "2026-07-01T00:00:00.000Z"
    }
  }
}
  • Query a past month with ?period=YYYY-MM (Europe/Berlin). A future period returns 400.
  • A limit of -1 means unlimited (Premium).
  • If the counter store (Redis) is briefly unavailable, you get a snapshot fallback with "stale": true instead of an error — treat it as approximate.

Build a simple warning: alert when used / limit >= 0.8. (The same threshold drives the X-Quota-Status: soft-warning header on every API response.)

Error handling

CodeHTTPWhat happenedFix
VALIDATION_ERROR400limit > 50, bad zip, or future periodFix the query parameters.
INSUFFICIENT_SCOPE403pk_ key on /usage, or missing scopeUse a secret key with usage:read.
ORIGIN_NOT_ALLOWED403Discovery pk_ call from a non-allowed domainAdd the domain in Settings → API.
NOT_FOUND404The relevant feature flag is offAsk to enable DISCOVERY_API_ENABLED/USAGE_API_ENABLED.
RATE_LIMITED429Discovery over 300 req / 300 sCache via ETags; respect Retry-After.

See the conventions reference.

Best practices

  • Cache aggressively with ETags — discovery is rate-limited to 300 requests per 300 seconds.
  • Page with cursors, never offsets; cursors are stable under concurrent inserts/deletes.
  • Watch the 80% line. Wire a usage alert before you hit the monthly limit.

Next steps