Docs
API guides

Export to a warehouse or BI tool

Land Resi data in Snowflake, BigQuery, or a spreadsheet for portfolio reporting.

Who this is for: client internal analytics teams and BI partners building occupancy, pricing, and marketing reporting across a portfolio.

What you'll build: a scheduled extract that lands Resi entities in your warehouse with enough history to trend availability and pricing over time.

What Resi can and cannot give you

It can give you the current state of every property, building, floor plan, unit, amenity, fee, review, and content record in an account, plus lead sources and connection configuration, all through V2.

It cannot give you history. The API returns current state only: there is no "availability as of last Tuesday" endpoint and no change log. If you want trends, snapshot on ingest: every extract writes a dated row. Miss a day and that day is gone for good.

Leads, tour bookings and demand events are not readable through the API. The API accepts them but has no endpoint that lists them, so a warehouse built on the API covers inventory, pricing and content, not the lead funnel.

The snapshot decision is the single most important one in a Resi BI pipeline, and it has to be made before the first run.

Extract design

Two tiers, matching what the API supports:

TierResourcesMethodCadence
Full snapshotproperties, buildings, floor plans, unitsFull scan (no updated_at filter)Hourly or daily
Incrementalamenities, fees, reviews, announcements, content blocks, FAQs, galleries, neighborhood places, lead sources, connections?updated_at[gte]=Daily, plus a weekly full scan

An updated_at query never returns a deleted record, so the incremental tier cannot see deletions. The weekly full scan is what catches them.

import os, requests
from datetime import datetime, timezone

BASE = "https://v2.getresi.com/api/v2"
HEADERS = {"Authorization": f"Bearer {os.environ['RESI_TOKEN']}", "Accept": "application/json"}

def collect(path, **params):
    params.setdefault("per_page", 200)
    rows, url, account_id = [], f"{BASE}{path}", None
    while url:
        res = requests.get(url, headers=HEADERS, params=params, timeout=30)
        res.raise_for_status()
        account_id = res.headers["X-Resi-Account-Id"]
        body = res.json()
        rows.extend(body["data"])
        url = body["links"]["next"]   # carries only ?page=N; params are sent again
    return rows, account_id

extracted_at = datetime.now(timezone.utc).isoformat()
units, account_id = collect("/units", sort="number")
for u in units:
    u["_extracted_at"] = extracted_at
    u["_account_id"] = account_id

links.next carries only the page parameter, not your filters or per_page. Send your parameters again with every page, as above. If you drop them, the second page onward comes back unfiltered at the default page size of 15.

dim_property        current state, SCD-2 on name / description / enabled / address
dim_building        current state
dim_floor_plan      current state
dim_unit            current state, SCD-2 on floor_plan_id / building_id

fact_unit_snapshot  ← the important one: one row per unit per extract
                      (unit_id, extracted_at, is_available, is_enabled, is_hidden,
                       is_model, is_guest_suite,
                       min_rent, max_rent, min_base_rent, max_base_rent,
                       available_at, vacate_at, made_ready_at, specials, updated_at)

dim_amenity, dim_fee, fact_review, dim_lead_source

fact_unit_snapshot is what makes the pipeline worth building. From it you can derive:

  • Occupancy and availability over time: count of available units by day, by property
  • Rent trend: average and median min_rent by floor plan by week
  • Days on market: the span between a unit first appearing available and disappearing
  • Concession tracking: when specials appears and disappears
  • Turn time: vacate_at → made_ready_at → available_at

None of these are answerable from a single extract. All are trivial once you have the fact table.

Metrics that need care

Rent. 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. Load both and label them clearly. Properties also differ in which one they advertise (pricing_display_settings.pricing_display_mode on the property), so store that per property if a dashboard should match what renters see.

Rents are numbers or null, and max_rent is often null. Keep null as null: if it becomes 0, your averages will be silently wrong.

"Available" is several flags, not one.

CASE WHEN is_enabled AND is_available AND NOT is_hidden
       AND NOT is_model AND NOT is_guest_suite
     THEN 1 ELSE 0 END AS is_marketable

Store the raw flags and derive the metric in the warehouse, so the definition is visible and changeable.

Unit counts. Total units ≠ marketable units ≠ leasable units. Model apartments, guest suites, and disabled records all inflate a naive count. Agree the definition with the business before publishing a number anyone will quote.

Multi-account portfolios

A token is pinned to one account, and every V2 call runs against that account by default. If the token's user belongs to several accounts, pass account_id to run a call against another of them: one token can extract them all. GET /api/v2/accounts lists the accounts the user can reach.

Every V2 response names the account it ran against in the X-Resi-Account-Id header. Stamp it on every row at ingest, as the collect() above does. Most payloads do not include an account id, and without it you cannot tell two accounts' data apart once it is in the same table.

Fields V2 does not carry

  • Fee-by-fee breakdown. V2 units and floor plans carry the itemized lines behind the total in fee_breakdown, with monthly_fee_total and upfront_fee_total alongside, as Resi last stored them (fees_synced_at). Load the lines into their own table keyed by unit or floor plan id and fee_id.
  • Availability rollups. Counts such as availableUnitsCount are on GET /api/v1/property/{property}. You can also derive them from fact_unit_snapshot.

The property's street address and coordinates are on the V2 property as address, so no V1 call is needed for mapping.

Scheduling and cost

  • 120 requests/minute per user. A daily full extract of a 5,000-unit portfolio is about 30 requests. Hourly is fine too.
  • Use a dedicated token created by a separate service-account user. The limit is per user, so the extract's budget stays apart from live integrations.
  • Run off-peak and stagger accounts rather than firing them all at once.
  • Log row counts per resource per run and alert on a swing beyond about 10%. A sudden drop usually means a partial scan, not a real change, and a partial scan loaded as truth will corrupt the trend for that day.

Checklist

  • Snapshot table designed before the first run
  • _extracted_at stamped on every row
  • Account id from X-Resi-Account-Id stamped on every row
  • Query parameters re-sent on every page
  • Weekly full scan to catch deletions
  • null rents preserved, never coerced to 0
  • Both min_rent and min_base_rent loaded and labeled
  • Marketability derived in SQL from raw flags
  • Dedicated service-account token
  • Row-count alerting on every run

Last updated on

On this page