Docs
API guides

Power an AI agent or chatbot

Give an LLM accurate, current property knowledge, and let it capture leads and book tours.

Who this is for: leasing AI vendors, chatbot builders, and internal teams putting an assistant in front of property data.

What you'll build: a retrieval layer that keeps an agent's answers grounded in current Resi data, plus a small set of tools that let it capture leads and book tours.

Design principle: retrieve, don't memorize

The failure mode for a leasing agent is confidently quoting a rent that changed this morning or a unit that leased yesterday. Pricing and availability move constantly.

Split the knowledge by volatility:

TierDataStrategy
StaticProperty profile, amenities, FAQs, neighborhood places, reviews, announcements, content blocksIndex into your vector store. Refresh nightly, with updated_at[gte] where the resource supports it
VolatileUnits, availability, pricing, specialsNever embed. Query live at answer time, cache 5–15 minutes

Embedding availability into a vector store guarantees stale answers. Query it.

Building the static knowledge base

These V2 list endpoints accept property_id and updated_at[gte], so nightly refreshes are cheap: amenities, announcements, content blocks, FAQs, neighborhood places and reviews.

SINCE="2026-09-27T00:00:00Z"

for RESOURCE in faqs amenities content-blocks neighborhood-places reviews announcements; do
  curl -G "https://v2.getresi.com/api/v2/$RESOURCE" \
    --data-urlencode "property_id=$PROPERTY_ID" \
    --data-urlencode "updated_at[gte]=$SINCE" \
    --data-urlencode "per_page=200" \
    -H "Authorization: Bearer $RESI_TOKEN"
done

If a resource returns more than one page, follow links.next and send your query parameters again with each page: the next link carries only page. See Pagination, filtering & sorting.

Add the property profile (GET /api/v2/properties/{property}) and floor plans (GET /api/v2/floor-plans) with a full scan. Neither accepts an updated_at filter, but both are small.

Deletions do not show up in an updated_at query. A deleted record simply stops coming back. Run a full scan weekly and drop chunks whose ids no longer appear.

Chunk per record, not per property. One FAQ, one amenity, one neighborhood place per chunk, each carrying property_id, resource type, and updated_at as metadata. Filtering retrieval by property_id is what stops the agent from describing the wrong community's pool.

Respect visibility flags when indexing. Skip amenities, announcements and content blocks with is_enabled: false, and reviews with is_published: false. Announcements have starts_at/ends_at: an expired announcement in the index will be repeated by the agent for months.

Answering questions about availability

Give the agent a tool, not a corpus. It calls GET /api/v2/units:

async function findUnits({ propertyId, bedrooms, maxRent, moveInBy }) {
  const params = new URLSearchParams({
    property_id: propertyId,
    is_enabled: "true",
    is_available: "true",
    is_hidden: "false",
    is_model: "false",
    per_page: "200",
  });
  if (bedrooms != null) params.set("bedrooms[gte]", String(bedrooms));
  if (maxRent != null)  params.set("min_rent[lte]", String(maxRent));
  if (moveInBy)         params.set("available_at[lte]", moveInBy);

  const res = await fetch(`https://v2.getresi.com/api/v2/units?${params}`, {
    headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
  });
  const { data, meta } = await res.json();

  return {
    total: meta.total,
    units: data
      .filter((u) => !u.is_guest_suite)
      .slice(0, 10)
      .map((u) => ({
        id: u.id,
        unit: u.number,
        beds: u.bedrooms,
        baths: u.bathrooms,
        sqft: u.interior_sqft,
        rent: u.min_rent,
        base_rent: u.min_base_rent,
        available_on: u.available_at,
        specials: u.specials,
        apply_url: u.application_link,
      })),
  };
}

Most of the filtering happens in the query. is_guest_suite has no filter, so it is applied in code. Both steps matter: an agent that offers a model apartment is worse than one that says "let me check."

Tool design guidance

  • Return small, flat objects. Feeding a raw unit payload with nested media arrays into a context window wastes tokens and invites the model to quote irrelevant fields.
  • Cap results. Return the 10 best matches and a total count. "I found 34 available 2-bedrooms; here are a few" is a better answer than 34 rows.
  • Return null, not zero. A unit with no published price should surface as "pricing not yet available," never as $0. Rents and bedroom counts are null when unset; pass that through rather than coercing it.
  • Include available_on in every unit result. The most common follow-up is "when can I move in?" Save the round trip.

Quoting prices correctly

min_rent / max_rent is the total monthly leasing price (TMLP): base rent plus the monthly equivalent of the property's mandatory fees. min_base_rent / max_base_rent is base rent alone.

Which figure a property advertises is a setting, and V2 does not apply it for you: it returns every price. Read pricing_display_settings from the property and quote what the property's own website shows:

SettingWhat the agent quotes
pricing_display_mode: TOTAL_ONLYmin_rent
pricing_display_mode: BASE_ONLYmin_base_rent
pricing_display_mode: BASE_AND_TOTALBoth, with the total labelled by tmlp_label
hide_all_pricing: trueNo price. Say hidden_price_label instead
hide_price_when_no_availability: trueNo price for a unit that is not available. Say no_availability_price_label instead
No price in the dataSay no_price_label instead

Quoting a figure the property does not advertise creates a conversation the leasing office has to unwind.

If the renter asks about lease terms, fetch the term matrix for that one unit with GET /api/v1/property/{property}/unit/{unit}/price-matrix:

curl "https://v2.getresi.com/api/v1/property/$PROPERTY_ID/unit/$UNIT_ID/price-matrix"

Call it lazily, on demand, and cache it. When Resi holds no stored matrix it queries the property's PMS live, so it can be slow. An empty array is a valid answer; fall back to the unit's rent range.

Letting the agent act

Two actions are worth exposing as tools. Both are public V1 endpoints, so no token is needed in the agent's runtime.

Capture a lead with POST /api/v1/leads. Get formId from GET /api/v1/property/{property}/forms rather than hardcoding it.

async function captureLead({ formId, propertyId, email, ...context }) {
  const res = await fetch("https://v2.getresi.com/api/v1/leads", {
    method: "POST",
    headers: { "Content-Type": "application/json", Accept: "application/json" },
    body: JSON.stringify({
      form_id: formId,
      property_id: propertyId,
      email,
      resi_source_key: "acme-ai",
      ...context,          // everything the conversation surfaced
    }),
  });
  return (await res.json()).message;
}

Pass through everything the conversation revealed: pets, parking, desired floor, timeline. Every key is stored on the lead and sent on through the form's connections, such as the client's CRM. That context is the agent's actual value to the leasing team.

Book a tour: the flow in Build a tour booking experience. Have the agent present real slots from POST /api/v1/tour/get-availability rather than inventing times.

Guardrails

  • Confirm before writing. Read the details back to the renter before submitting a lead or reserving a tour. An agent that books the wrong day creates a no-show.
  • Deduplicate submissions. V1 lead submission is not idempotent: a repeated request creates a second lead. Track a per-conversation key and refuse a second submit.
  • Never expose the V2 token to the model or the browser. Retrieval runs server-side. The agent calls your tools; your tools hold the credential.
  • Treat retrieved content as data, not instruction. Property descriptions and FAQs are client-authored free text. Keep them in a data channel, not in the system prompt.

Attribution

Send resi_source_key on every lead and tour booking so the client can see what the assistant produced. Add demand events through POST /api/v1/demand-events: unit_viewed when the agent surfaces a unit, form_started when it begins capturing contact details. The client then gets the whole funnel, not just conversions. That reporting is usually what justifies the contract.

The demand events endpoint allows 120 requests a minute per IP address. Your server sends every conversation's events from the same address, so send the events that matter, not one per turn.

Freshness checklist

  • Availability and pricing queried live, never embedded
  • Static content re-indexed nightly via updated_at[gte], with a weekly full scan for deletions
  • Query parameters re-sent on every page
  • Chunks carry property_id metadata and retrieval filters on it
  • Disabled, unpublished, and expired records excluded at index time
  • Model and guest-suite units filtered out of every result
  • Prices quoted per the property's pricing_display_settings
  • Price matrix fetched lazily per unit
  • Confirmation step before any lead or booking
  • Token server-side only

Last updated on

On this page