Headless quickstart
Keep your listings in the system you already use (a real estate CRM, a back office, a spreadsheet export) and let letsrealty publish them: on your letsrealty website, through the API, and on any other website you have through widgets. This guide walks the whole path once; each step links to the guide with the details.
- Listings: what a listing looks like when you read it.
- Writing listings: create and update listings by your own reference.
- Publishable keys: read your live listings from a browser.
- Widgets: show them on your own website with a snippet.
1. Create an API Key
In the Admin Console, a Tenant Admin opens Integrations → API keys, creates a key for the sync and gives it the scopes properties:write (which includes properties:read) and webhooks:manage. Add leads:read to hear about the Leads your widgets send. The key is shown once; keep it on your server as LETSREALTY_API_KEY. See Authentication & scopes.
2. Sync your listings
PUT /v1/properties/by-ref/{internal_ref} creates the listing with your reference the first time (201) and updates it after that (200), so a sync job sends every listing the same way every time:
curl -X PUT "https://api.letsrealty.io/v1/properties/by-ref/CASA-TAM-07" \
-H "Authorization: Bearer $LETSREALTY_API_KEY" \
-H "Idempotency-Key: nightly-2026-10-04-CASA-TAM-07" \
-H "Content-Type: application/json" \
-d '{
"title": "Tamarindo beach house",
"description": "Three bedrooms, a pool and a view of the bay.",
"listing_type": "sale",
"price": "385000",
"currency": "USD",
"bedrooms": 3,
"place_code": "CR-50309",
"ai_enrichment": false
}'
A listing with a place_code goes on your website. With "ai_enrichment": false nothing is translated or rewritten for you, so send each language's text yourself: the same call with ?lang=es writes the Spanish text, which AI runs then leave alone.
curl -X PUT "https://api.letsrealty.io/v1/properties/by-ref/CASA-TAM-07?lang=es" \
-H "Authorization: Bearer $LETSREALTY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Casa de playa en Tamarindo", "description": "Tres habitaciones, piscina y vista a la bahía.", "ai_enrichment": false }'
The same job in Node, with each listing's photos imported from the URLs your system already hosts (the same URL again adds nothing):
const API = "https://api.letsrealty.io";
const headers = {
Authorization: `Bearer ${process.env.LETSREALTY_API_KEY}`,
"Content-Type": "application/json",
};
async function call(method, path, body) {
const res = await fetch(`${API}${path}`, { method, headers, body: JSON.stringify(body) });
if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${await res.text()}`);
return res.status === 204 ? null : res.json();
}
export async function sync(listings) {
for (const listing of listings) {
const ref = encodeURIComponent(listing.ref);
const written = await call("PUT", `/v1/properties/by-ref/${ref}`, {
title: listing.title.en,
description: listing.description.en,
listing_type: "sale",
price: String(listing.price),
currency: "USD",
bedrooms: listing.bedrooms,
place_code: listing.placeCode,
ai_enrichment: false,
});
await call("PUT", `/v1/properties/by-ref/${ref}?lang=es`, {
title: listing.title.es,
description: listing.description.es,
ai_enrichment: false,
});
for (const [index, url] of listing.photos.entries()) {
await call("POST", `/v1/properties/${written.id}/images:import`, { url, is_primary: index === 0 });
}
if (listing.sold) await call("DELETE", `/v1/properties/${written.id}`);
}
}
Archiving with DELETE /v1/properties/{property_id} takes a listing off your website and out of your widgets; its slug and price history are kept.
3. Hear about changes
Subscribe an endpoint of yours to the listing events with POST /v1/webhooks/subscriptions. Keep the secret it answers with to verify each delivery:
curl https://api.letsrealty.io/v1/webhooks/subscriptions \
-H "Authorization: Bearer $LETSREALTY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://crm.youragency.com/hooks/letsrealty",
"events": ["property.created", "property.went_live", "property.price_changed", "property.archived", "lead.created"]
}'
The first sync sends property.created and, for a listing with a Place, property.went_live; a new price sends property.price_changed with the previous_price; an archive sends property.archived. A widget's Inquiry sends lead.created. See Webhooks.
4. Read them back
GET /v1/properties with updated_since returns what changed, in the language lang asks for, so another system can keep its own copy:
curl "https://api.letsrealty.io/v1/properties?updated_since=2026-10-04T00:00:00Z&lang=es" \
-H "Authorization: Bearer $LETSREALTY_API_KEY"
Page with next_cursor until it is null. See Listings and Pagination & sync.
5. Show them on your website
In Integrations → Website widgets, add your website's origin (https://www.youragency.com), pick a widget and copy the snippet. Paste it into any page:
<script type="module" src="https://www.letsrealty.io/widgets/v1/lr.js" data-key="lr_pk_1a2b3c4d_xxxx"></script>
<lr-listings layout="grid" listing="sale" limit="6"></lr-listings>
<lr-lead-form></lr-lead-form>
The page shows your live listings in your brand and language; a visitor opens one in a modal and writes to you, and the Inquiry is a Lead "via widget" with the page it came from. The key in the snippet is a publishable key: it works only on the origins you add, so the same snippet on any other website reads nothing. A change you sync reaches your widgets within about a minute.
To build your own front end instead, read the same listings with the publishable key from the browser: see Publishable keys.