Docs
API guides

Build a property website

Use the Sites API for a new server-rendered property or leasing site, and V1 for WordPress sites and widgets.

Who this is for: agencies and web partners building property marketing sites, and internal teams building custom front ends.

What you'll build: a property site, or a centralized leasing site for a portfolio, that renders every section from Resi and stays current without redeploys.

Pick the API first

You are buildingUse
A new Next.js (or other server-rendered) property website or centralized leasing siteSites API
An existing WordPress site, or a widget embedded on a pageV1

The two are not interchangeable. The Sites API authenticates one website with its own token, is called only from the site's server, addresses records by slug, and tells you when to revalidate. V1 needs no credential, is addressed by property UUID, and is safe to call from a browser. See Which API should I use?

New sites: the Sites API

Base URL: https://v2.getresi.com/api/sites/v1

A token belongs to one website, and every response is limited to that website's own properties, floor plans, units, content, forms and integrations. A property website sees its one property; a centralized leasing site sees its whole portfolio through the same endpoints.

Server-side only. The website token reads data that is not public and can create leads. Call the API from route handlers, server components or build steps. It must never reach a browser.

A typical build

  1. GET /site: domains, tracking, redirects, URL patterns and a card per property.
  2. GET /paths: every path to generate statically, and the sitemap.
  3. Per page: GET /properties/{property}, GET /floor-plans, GET /units, GET /entries/{type}/{slug}.
  4. Per property: GET /integrations, GET /lead-sources, GET /forms.
  5. At request time: GET /resolve for paths the build did not generate, GET /availability for what moves, and the POST endpoints for form submissions, tours and analytics.
async function resi<T>(path: string, tags: string[], revalidate: number): Promise<T> {
  const res = await fetch(`${process.env.RESI_DELIVERY_API_URL}${path}`, {
    headers: {
      Authorization: `Bearer ${process.env.RESI_DELIVERY_TOKEN}`,
      'X-Resi-Website': process.env.RESI_WEBSITE_ID!,
      Accept: 'application/json',
    },
    next: { tags, revalidate },
  });
  if (!res.ok) throw new Error(`Resi ${path}: ${res.status}`);
  return res.json();
}

const units = await resi(`/units?property=${slug}`, [`inventory:${propertyId}`], 300);

Staying current

Every GET carries meta.cache.tags (also in X-Resi-Cache-Tags) and a suggested meta.cache.ttl. Cache responses in your application under those tags. When data a site rendered changes, Resi POSTs a signed cache-clear webhook naming the tags that changed to any site set up to receive it (every site scaffolded from the current template is); verify the signature and revalidate those tags. You do not need to poll or rebuild on a timer. See Caching & the cache-clear webhook.

Leads, tours and analytics

Forms render from GET /forms, and submissions go through POST /forms/{form}/submissions from your server, with an Idempotency-Key so a retry after a timeout cannot create a second lead. Tours use POST /tours/availability and POST /tours/reservations. Writes are limited to 60 a minute per website, reads to 1,200. See Forms, tours & analytics.

Where to go next

WordPress sites and widgets: V1

The public V1 delivery API returns pre-composed payloads per property. It needs no credential, so a WordPress theme, a plugin or a widget can call it directly, including from the browser.

Base URL: https://v2.getresi.com/api/v1 · See Website & content data for the full endpoint map.

Page-by-page fetch plan

PageEndpoints
Home/property/{id}, /galleries, /announcements, /content-groups, /panels
Floor plans/floor-plans, /unit-types
Availability/units, /floor-plans
Unit detail/units, /unit/{unit}/price-matrix (lazy)
Amenities/amenities, /galleries
Neighborhood/neighborhood
Reviews/reviews
FAQ/faqs
Contact / Tour/forms, /integrations
const base = `https://v2.getresi.com/api/v1/property/${propertyId}`;

const [property, galleries, announcements, content, panels] = await Promise.all([
  fetch(base).then((r) => r.json()),
  fetch(`${base}/galleries`).then((r) => r.json()),
  fetch(`${base}/announcements`).then((r) => r.json()),
  fetch(`${base}/content-groups`).then((r) => r.json()),
  fetch(`${base}/panels`).then((r) => r.json()),
]);

These are independent, so fire them concurrently.

What to cache, and for how long

DataWhen to fetch
Property profile, amenities, galleries, FAQs, neighborhood, reviews, content, panelsCache for hours; refresh at least daily
AnnouncementsCache for an hour at most
Floor plans, unit typesRevalidate hourly
Units, availability, pricingRequest time, or a cache of 5–15 minutes
Price matrixRequest time, on unit detail only, cached per unit per day
Forms, integrationsRevalidate hourly

Baking availability into a long-lived cache is the classic mistake: the site shows a unit that leased three days ago, and the leasing office fields the call.

V1 returns only announcements that are active at request time, and the payload carries no start or end dates. A page cached for a day keeps showing an announcement that ended this morning, so keep that cache short.

Purging the site's host cache. POST /api/v1/cache/clear purges the page cache of the website host connected to one property (property_id) or one portfolio (portfolio_id). That is a Kinsta or WP Engine connection, plus the site of any merged parent property:

curl -X POST https://v2.getresi.com/api/v1/cache/clear \
  -H "Content-Type: application/json" \
  -d '{"property_id": "019dd604-8468-7388-9a52-4c31a2e1209a"}'

It always answers 202, whether or not anything was purged: a property with no Kinsta or WP Engine connection has nothing to clear. It does not reach any other page cache or CDN, so clear those alongside it. Rate limit: 30 a minute per target.

Rendering guidance

Images. Image objects carry thumb_url, full_url, alt_text and caption. full_url is a resized copy generated on a queue, so for a short time after an import it can return a 404; thumb_url exists as soon as the import finishes. Fall back to it on error. Use alt_text as your alt attribute; where it is empty, prefer an empty alt="" on decorative images over inventing a description.

Availability display. /units returns the property's enabled units. Show a unit only when unitAvailable is true and unitModel and unitGuestSuite are false. Group by floor plan for the standard "3 available in the A1 layout" presentation, and use /unit-types for the bed/bath summary rollups.

Pricing. unitMinTmlp (also unitPrice) is the total monthly leasing price: base rent plus the monthly equivalent of mandatory fees. unitMinBaseRent is base rent alone. Follow the property's pricingDisplaySettings: pricing_display_mode says whether to show the total, the base, or both, and tmlp_label labels the total. A price can come back null. Render hidden_price_label when the property hides all pricing, no_availability_price_label when it hides prices with nothing available, and no_price_label for any other null price. Never compute your own total from base rent plus fees. Where the maximum is null, show a single price rather than a range with an empty side.

Content groups and panels carry the client's configured page sections. Render them in the order returned. Treat an unrecognized block type as a no-op rather than an error, since new types get added without a version bump.

Lead form and tour widget

Render the form from /forms rather than hardcoding fields; clients change forms without telling their agency. Submit to POST /api/v1/leads and display the returned message. See Capture leads from your own site or CRM.

For tours, use the flow in Build a tour booking experience, and hide the tour UI entirely when /integrations lists no connection with category: "tour".

Add your own spam protection in front of the lead form. Resi does not rate-limit V1 lead submission, and it has no idempotency: a double submit creates two leads.

SEO and structured data

The V1 property payload has everything needed for schema.org/ApartmentComplex:

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "ApartmentComplex",
  name: property.name,
  description: property.description,
  url: property.website,
  telephone: property.phone,
  image: property.imageFull,
  address: {
    "@type": "PostalAddress",
    streetAddress: property.street,
    addressLocality: property.city,
    addressRegion: property.state,
    postalCode: property.zipcode,
  },
  geo: {
    "@type": "GeoCoordinates",
    latitude: property.latitude,
    longitude: property.longitude,
  },
  numberOfAvailableAccommodationUnits: property.availableUnitsCount,
};

Reviews from /reviews can populate aggregateRating, but only publish ratings you are permitted to syndicate. Some review sources prohibit re-display; confirm with the client before marking them up.

Content privacy

V1 delivers published marketing content to anyone who asks, so treat every field it returns as public. If a client keeps internal notes or staging URLs in a rendered content field, those travel to the page. Worth raising once during onboarding so internal notes stay in internal fields.

Checklist

  • Sites API for new server-rendered sites; V1 for WordPress and widgets
  • Sites API token kept on the server
  • Sites API responses cached by tag and cleared by the webhook
  • V1: static content cached long, availability at request time, announcements short
  • Concurrent fetches, not sequential
  • Image fallback when full_url is not ready
  • alt_text used for alt
  • Marketability flags applied before rendering a unit
  • Prices shown per the property's display settings, never a self-computed total
  • Unknown content block types ignored, not fatal
  • Lead form rendered from /forms with spam protection
  • Tour UI hidden when no tour connection exists

Last updated on

On this page