Skip to content

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

Embed listings on your website

Show your live WOHNO listings on an external site (WordPress, plain HTML) with a browser-safe publishable key and an origin allowlist.

This guide takes you from zero to a working listings grid on your own website — WordPress, a static page, or any front-end framework. You will use a publishable key that is safe to ship in the browser, locked to your domain with an origin allowlist, and read your organization's listings over https://wohno.de/api/v1.

The problem this solves: you maintain your listings in WOHNO but want them to appear on your agency's marketing site, kept in sync automatically, without running a server.

Prerequisites

  • A WOHNO account with at least one listing.
  • A publishable key (pk_live_…). Publishable keys are read-only and require an origin allowlist — that is what makes them safe to expose in a browser bundle.
  • The scope listings:read.
  • The LISTINGS_PUBLIC_API_ENABLED feature is generally available — no extra flag needed.

Never use a secret key (sk_…) in browser code. Secret keys carry write and delete scopes; exposing one would let anyone mutate your data.

Step 1 — Create a publishable key with an origin allowlist

  1. Open Dashboard → Settings → API.
  2. Click Create API key and choose Publishable key (pk_…).
  3. Grant the listings:read scope.
  4. Add your site's origins to the origin allowlist, for example https://www.example.com. A subdomain wildcard such as https://*.example.com matches exactly one subdomain level. HTTPS is required (except http://localhost for local testing).
  5. Save and copy the key. Publishable keys are browser-safe, so you can paste this one straight into your front-end.

The allowlist is enforced server-side: browsers automatically send the Origin header, and a call from a domain that is not on the list is rejected with 403 ORIGIN_NOT_ALLOWED. A pk_ call with no Origin header at all returns 403 ORIGIN_REQUIRED.

Step 2 — Fetch listings from the browser

Send a GET to /api/v1/listings with the key in the X-API-Key header. The endpoint is cursor-paginated and accepts filters:

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

A successful response (200):

{
  "data": [
    {
      "id": "9f1c2a3b-1111-2222-3333-444455556666",
      "title": "Bright 2-room apartment",
      "city": "Berlin",
      "zip": "10115",
      "rooms": 2,
      "rent_cold": 980,
      "property_type": "apartment",
      "status": "published"
    }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6...",
    "has_more": true,
    "limit": 12
  }
}

Available filters include city, zip, rooms_min, rooms_max, rent_max, property_type, wbs_only, pets and a free-text q. limit ranges from 1 to 50 (default 20). See the listings reference for the full field whitelist.

Step 3 — Render the grid (vanilla JS embed)

Drop this snippet into any page — a WordPress Custom HTML block works the same as a static .html file:

<div id="wohno-listings"></div>
<script>
  const WOHNO_PK = "pk_live_xxxxxxxxxxxxxxxxxxxxxxxx";
 
  async function loadListings() {
    const res = await fetch(
      "https://wohno.de/api/v1/listings?city=Berlin&limit=12",
      { headers: { "X-API-Key": WOHNO_PK } },
    );
 
    if (!res.ok) {
      const body = await res.json();
      console.error("WOHNO API error:", body.error.code);
      return;
    }
 
    const { data } = await res.json();
    const root = document.getElementById("wohno-listings");
    root.innerHTML = data
      .map(
        (l) => `
        <a class="wohno-card" href="https://wohno.de/inserate/${l.id}">
          <h3>${l.title}</h3>
          <p>${l.city} · ${l.rooms} rooms · ${l.rent_cold} € cold</p>
        </a>`,
      )
      .join("");
  }
 
  loadListings();
</script>

The browser supplies the Origin header automatically, so you do not set it in fetch — it is added by the browser and validated against your allowlist.

Step 4 — Paginate

To load more, pass the next_cursor from the previous response as ?cursor= while pagination.has_more is true:

curl "https://wohno.de/api/v1/listings?city=Berlin&limit=12&cursor=eyJpZCI6..." \
  -H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Origin: https://www.example.com"

Error handling

Branch on the stable error.code, never the message text:

CodeHTTPWhat happenedFix
ORIGIN_REQUIRED403pk_ key called without an Origin headerCall from a browser, not a bare server script.
ORIGIN_NOT_ALLOWED403Calling domain is not on the key's allowlistAdd the domain in Settings → API.
INSUFFICIENT_SCOPE403Key lacks listings:readGrant the scope.
RATE_LIMITED429Over 1000 requests/hour for this keyCache results; respect Retry-After.

See the conventions reference for the full error table.

Best practices

  • Cache responses. Listings change slowly; the endpoint returns an ETag. Send it back as If-None-Match to get a cheap 304 Not Modified.
  • One key per site. Scope each publishable key to a single origin so you can rotate it without breaking other embeds.

Next steps