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.

  1. Listings: what a listing looks like when you read it.
  2. Writing listings: create and update listings by your own reference.
  3. Publishable keys: read your live listings from a browser.
  4. 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.