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, nothttps://www.youragency.com/listings. https://*.youragency.comallows every subdomain ofyouragency.com. Listhttps://youragency.comseparately 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 itsOriginheader). 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-Originset to itself. - A request without an
Originheader, from a script orcurl, is served too, and may leaveoriginout: the key only opens what your website already shows. If it sendsorigin, 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 filterslisting,location,type,min_price,max_price,beds,amenities,q,featuredandsort, andlimitandoffset.GET /v1/site/properties/by-slug/{slug}— one listing by its slug. A retired slug still finds it, withis_canonical: false.GET /v1/site/properties/by-id/{property_id}— one listing byid:availablewith its detail, orunavailableonce it is off the market.GET /v1/site/properties/by-slug/{slug}/similarandGET /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, andGET /v1/site/locations/{slug}for one of them.GET /v1/site/agentsandGET /v1/site/agents/by-slug/{slug}— your active Agents' public profiles.GET /v1/site/amenities— the amenity vocabulary, labelled inlocale.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).