Publishable keys

A publishable key lets a web page read your live listings, Agents, Locations and amenities straight from the browser, and send an Inquiry, without a server of your own. Website widgets use one. Unlike an API Key, it is not a secret: anyone can see it in your page's source, so it only opens what your website already shows, and only to the websites you list.

Creating one

A Tenant Admin creates it in the Admin Console under Integrations → API keys → Publishable keys, and lists the allowed origins: the websites that may use it, such as https://www.youragency.com.

  • An origin is https:// plus the host, with no path: https://www.youragency.com, not https://www.youragency.com/listings.
  • https://*.youragency.com allows every subdomain of youragency.com. List https://youragency.com separately if you also use the bare domain.
  • http://localhost (any port) is allowed too, for trying it on your own machine.

The key reads lr_pk_… and is shown in the console whenever you need it. Edit its origins at any time; revoke it to switch it off for good.

Calling the API

Send the key as ?key= instead of an Authorization header, and your page's origin as &origin=, from a page on an allowed origin:

const url = new URL("https://api.letsrealty.io/v1/site/properties");
url.searchParams.set("key", "lr_pk_1a2b3c4d_xxxx");
url.searchParams.set("origin", window.location.origin);
url.searchParams.set("listing", "sale");
url.searchParams.set("limit", "12");
url.searchParams.set("locale", "en");
const res = await fetch(url);
const { items, total } = await res.json();
  • From a browser, every read needs origin, set to exactly the page's origin (window.location.origin, which the browser also sends as its Origin header). Leaving it out, or sending another site's, is a 403. Widgets do this for you.
  • A request from a browser on an origin the key doesn't list is a 403, and gets no CORS headers, so the browser won't read it.
  • An allowed origin gets Access-Control-Allow-Origin set to itself.
  • A request without an Origin header, from a script or curl, is served too, and may leave origin out: the key only opens what your website already shows. If it sends origin, that must be an allowed one.
  • An unknown or revoked key is a 404, and a suspended account a 503.

Never put a secret API Key (lr_live_…) in a URL. One sent as ?key= is refused with a 400 and marked in the console as exposed; rotate it straight away.

What you can read

  • GET /v1/site/properties — a page of live listings, with the filters listing, location, type, min_price, max_price, beds, amenities, q, featured and sort, and limit and offset.
  • GET /v1/site/properties/by-slug/{slug} — one listing by its slug. A retired slug still finds it, with is_canonical: false.
  • GET /v1/site/properties/by-id/{property_id} — one listing by id: available with its detail, or unavailable once it is off the market.
  • GET /v1/site/properties/by-slug/{slug}/similar and GET /v1/site/properties/{property_id}/similar — up to 3 or 6 listings like it.
  • GET /v1/site/locations — the Areas, Places and property types that have live listings, with counts, and GET /v1/site/locations/{slug} for one of them.
  • GET /v1/site/agents and GET /v1/site/agents/by-slug/{slug} — your active Agents' public profiles.
  • GET /v1/site/amenities — the amenity vocabulary, labelled in locale.
  • GET /v1/theme — what widgets style themselves with: your brand's colours, fonts, corner style and logo, your Site Languages, your website's address and whether to show "Powered by letsrealty".

These answer exactly what your website shows: only live listings, an approximate location's coordinates rounded so a map shows an area rather than a pin, and never your internal reference. Pass locale to get a listing's text in one of your Site Languages. The full shapes are in the reference.

Freshness and caching

Every successful GET with a publishable key is sent with Cache-Control: public, s-maxage=60, stale-while-revalidate=300 and is cached at our edge, keyed by the whole URL. A change you make to a listing shows within about a minute; nothing needs purging. Two visitors who ask for the same URL share one cached answer, so keep your query strings stable. Because origin is part of the URL, each of your websites gets its own cached answer, with CORS for that website only: a page elsewhere that copies your URL gets an answer its browser refuses to read.

Sending an Inquiry

POST /v1/site/leads?key=…&origin=… sends an Inquiry, which lands in your CRM like one from your website: the same Lead matching, Lead Owner, Lead Scoring and email notification. It must carry a Turnstile token from Cloudflare's Turnstile widget on your page, in turnstile; without a valid one it is a 403.

const leads = new URL("https://api.letsrealty.io/v1/site/leads");
leads.searchParams.set("key", "lr_pk_1a2b3c4d_xxxx");
leads.searchParams.set("origin", window.location.origin);
await fetch(leads, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Lucía Vargas",
    email: "lucia@example.com",
    message: "Is it still available?",
    property_id: "2f0c9d4e-7a51-4c3b-9b1e-0d6a8f3e5c21",
    turnstile: turnstileToken,
  }),
});

Name a listing with property_id or an Agent with agent_id (at most one), or neither for a general inquiry. The Lead's source is Widget, with the page it was sent from as the browser's Referer gives it (browsers send only your site's origin across sites unless the request sets referrerPolicy: "no-referrer-when-downgrade"), and the Inquiry reads "via widget" in the console. POST /v1/site/properties/{slug}/view counts a view of a listing's detail. On these POSTs, origin is optional, but when you send it, it must be your page's origin too.

Limits

The same on every plan:

  • 120 requests a minute from each visitor's IP address, and 5 Inquiries a minute;
  • 3,000 requests a minute for all of your publishable keys together.

Answers served from the edge cache don't count. A spent budget is a 429 with Retry-After, and the RateLimit headers work as for API Keys (see Rate limits).