Overview  ·  ← Project plan
FOOTHOLD
BOISE APARTMENTS
Entrata integration

Rewritten 2026-08-05  Current, and here's what outranks it

This is the plain-English picture of the Entrata connection: what we read, what we don't, and how a lead actually moves. It was rebuilt on 2026-08-05 against two rounds of live read-only probes run from the VPS, so the old "here's what we'd ask for someday" version is gone. The API source of truth is reference/entrata-touchpoints.md, which holds every call, required params, window caps, measured rate limits, and verified response shapes. The build source of truth is system-design.html, which holds the Constitution, component statuses, and the phased build order. Where either disagrees with this page, they win.

Every write is blocked today, and that is a choice we made. The key carries 28 services: 22 usable reads, 4 that always error, and 2 writes, sendLeads and updateLeads. Liberty enabled those two on 2026-07-31 on a live, occupied building. Brent declined to have them removed, so they stay enabled by choice and there is no vendor-side guard: the deny-by-default read-method allowlist in our code, enforced at the one function that issues requests and failing safe, is the only control that exists or ever will. sendWorkOrders and updateWorkOrders were never granted, and were still withheld on 2026-08-03 when Liberty put the whole maintenance group on the key with two reads on it, so their absence is a deliberate per-method decision on Liberty's side and not something to lean on. Anything this design would write (a guest card, a note on a guest card, a work order) goes into our own outbox and stays there. Nothing is issued to Entrata without Brent's written per-property sign-off naming the property and the methods. Constitution rule 2: read-only against any live production property. 📌 Decision, Brent, 2026-08-05, and it does not loosen anything today: the rule will be lifted on NPI-owned properties when Foothold goes live on them (Aspen, then Summerset), and only when all three conditions hold: NPI owns it, Foothold is live on it, and Brent has signed off in writing naming that property and those methods. Owyhee Park is not covered and is not expected to be, since it is Liberty's building, so everything on this page stays read-only for Owyhee permanently. The allowlist becomes per-property rather than going away, and an unrecognised property config still means read-only. sendWorkOrders became a live ask the same day, for owned properties only, and it has never been made.

Entrata API: the connection points

Liberty Asset Group is the property manager and Entrata is their PMS. Access is live: one API key, read-only, in production since 2026-07-31, every call originating from the NPI VPS at 5.78.139.104. The key covers exactly one property: Owyhee Park, Liberty's own Boise building, 52 units across 4 buildings and 4 floor plans. Aspen and Summerset are not on it. Aspen closes 8/31 and isn't in Liberty's Entrata yet, Summerset either, so everything below is verified against Owyhee Park and is the shape our properties inherit the day they land on the key. North River's software is still being decided.

The data flow

Entrata (Liberty's PMS)

Units, floor plans, availability, pricing
Rent roll, leases, charges, pets
Leads, birth dates, ID vocabulary
Work orders + maintenance vocabulary
Marketing consent per channel
GL actuals
pull
(read, live)
outbox
(queued, unsent)

Foothold systems

footholdboise.com /rentals + /map
Apartments.com / Zillow feed freshness
AI speed-to-lead agent + GHL CRM
Recognition & turn engines
FlightDeck

Pull = the 22 usable read methods, called serially from the VPS and cached, never on a page view. Outbox = every write intent is written to a queue we own and held there unsent. The arrow points at a box on our side, not at Entrata. It only ever reaches Entrata after Brent signs off in writing, per property.

Connection points, and what we use them for

DataDirectionWhat we use it forStatus
Availability & pricing
getUnitsAvailabilityAndPricing
PullLive rent + available dates onto the Foothold site (/rentals, /map) so listings never go stale. Freshness is a ranking signal on Apartments.com. Entrata's availability is the one to trust, don't derive it from lease status, the two don't reconcile. Don't take square footage from this method, its numbers are broken.Reads live
Units, plans, unit types
getPropertyUnits, getFloorPlans, getUnitTypes
PullAll 52 units with building, floor, unit number and address; 4 floor plans with the authoritative square footage and CDN-hosted floorplan images. Rent comes back for all 24 lease terms but only the 12-month term is web-visible, so that's the one we quote. Display by floor plan, price by unit type.Reads live
Property basics
getProperties
PullOffice hours, lease terms offered, post months. Read the hours, never hardcode them. Owyhee reads M-F 9 to 5 with no weekend coverage, which is the number the whole after-hours argument rests on.Reads live
Amenities & specials
getAmenities, getSpecials
PullPermissioned, and they return zero rows. HTTP 200 with no data. Amenity copy and concession display cannot be synced, they're our own content with a named owner and a staleness date, and concessions are an out-of-band conversation with Liberty.200, no data
Leads
getLeads, getLeadPickLists
PullThe guest card CoStar already created, the speed-to-lead clock (createdOnDateTime), lead source mix, and the only working source of resident date of birth. Pick lists are the ID vocabulary to pin: 155 event types, 62 lead statuses, 15 named Liberty agents, 17 lead sources. Needs fromDate + toDate; takes a 730-day window, so 5 years of history is 3 calls. Highest-PII method on the key.Reads live
Guest cards + note logging
sendLeads, updateLeads
OutboxThe design writes our lead in as a guest card and mirrors the full word-for-word thread onto it as notes, so the PM works it in their normal queue. Both are writes. They queue in our outbox with real pick-list IDs and are never issued until Brent signs off for that property.Blocked, rule 2
ApplicationsLinkNever an API write and never was: the applications group isn't on the key at all. We hand the renter Entrata's own ProspectPortal apply link and follow up on stalled ones by watching lead status. Open item: the per-property apply URL from Liberty. Don't guess it.No API
Residents & leases
getLeases, getLeaseDetails, getMitsLeases
PullThe current rent roll (52 leases, 103 customer records, 88 current), scheduled charges, loss-to-lease against market rent, and renewal exposure by month. Best pet source on the key: 34 animals across 30 leases, with name, type, breed, color, age, weight. getMitsLeases gives household grain (100 people) and the customer IDs the consent read needs. Neither returns birth dates. ⚠️ Omit the status filter, filtering on it returns 190 cancelled leases instead of the roll.Reads live
Work orders
getWorkOrders, getWorkOrderPickLists
PullGranted 2026-08-03 and verified returning real production tickets. Feeds the turn engine and the "How'd we do?" trigger. Filter out Entrata PaaS records, those are system-generated and must never fire a resident survey. Collapse parent/child before counting. Pass petInfo to whoever gets sent to the unit. The pick list carries the priority/status/problem vocabulary and Liberty's 12-person staff roster; cache it, it's the tightest daily bucket on the key.Reads live
Marketing consent
getMarketingPreferences, getMarketingPreferencePickList
PullOpt-in per customer per channel (email, phone, postal mail, SMS) with consent type. This is the gate in front of every outbound touch, including physical mail. Works for both the lead side and the resident side, and one person can hold both sets, so check both and fail closed. A missing preference means don't send. Cache the pick list, it's the tightest hourly bucket on the key.Reads live
Tour calendar
getCalendarAvailability
PullReal, maintained tour configuration: 30-minute appointments, a hard 120-minute minimum lead time, agent-guided M-F 08:00 to 16:00 MST, self-guided all 7 days 06:00 to 18:00 MST at one at a time. ⚠️ 7-day maximum window, so a 60-day horizon costs 9 calls. ⚠️ Liberty books tours manually, so this is a template, not a booking ledger: offer windows, confirm with a human.Read, human confirms
Financials
getGlTransactions
PullGL actuals, 90 accounts, line items tagged to unit and building, which makes FlightDeck NOI and NPI's monthly reconciliation a drop-in. Budget comparison is not on the key, so NOI-vs-budget sources budget from our own model. ⚠️ Resident names sit in the AR payment memos, treat it as a PII feed, not "just numbers."Reads live

Not on the key, so don't design against it: the entire customers group, getLeadEvents, getPropertyMedia (property photo library), getExpiringLeases, lease documents, inspections, budgets and bank accounts, and the applications, arcodes, arpayments, artransactions, leasingcenter, pricing, queue and vendors groups in their entirety. Most have a workaround in the table above; none is worth an ask right now.

Build the integration behind a PMS adapter so North River can plug in a different source later without reworking the site or the agent.

How a lead flows in (and how the agent stays visible)

This is the path from "renter clicks inquire on Apartments.com" to "Taylor's team sees it," confirmed from CoStar + Entrata docs:

  1. Renter inquires on the Apartments.com listing (check availability / request tour / email). We get their real name, email, phone, and move-in date (masking is only on the public listing).
  2. CoStar delivers it two ways at once: (a) into Entrata as a guest card via the native Apartments.com→Entrata feed (set up through feeds@apartments.com, linking the Listing ID to the Entrata Property ID), and (b) as an email to up to 6 lead destinations you choose.
  3. We point one email destination at the AI agent (running through GHL), so it gets the lead instantly and replies in under 5 minutes from one dedicated number + inbox per property. We forward the property's published/tracking number to it, so the guest only ever sees one consistent number. No waiting on Entrata polling.
  4. The prospect replies into our GHL inbox, NOT Entrata. Their response comes back to the Foothold address/number the agent used. Entrata never sees it automatically, it only knows what we write to it.
  5. We read the guest card back out of Entrata with getLeads, which is how we dedupe against the card CoStar already created and time ourselves off createdOnDateTime, the real speed-to-lead clock. This half runs today. It's a read.
  6. Every message, both directions, is written as a timestamped note and held in our outbox. The finished design mirrors the full word-for-word thread onto the guest card via updateLeads so the on-site team reads exactly what the AI said, not a summary. That call is a write, so under rule 2 it does not go out: the note queues locally and replays only once writes are signed off for that property. Which means that until sign-off, the guest card does not show the thread. That's a real gap, not a rounding error, and it's the price of the read-only rule.

The transcript is ours. The Entrata mirror is deferred, not cancelled

The authoritative full transcript sits in our own system (GHL / the agent's datastore), which we own, so the accountability trail never depended on Entrata in the first place. Every message the agent sends and every reply it gets is logged as its own note. Logging a note and sending a message are different operations, and the agent only ever writes notes, so the prospect never gets a duplicate.

Build the queued notes against the real ID vocabulary now. getLeadPickLists hands over 155 event types, 62 lead statuses, 15 named leasing agents and 17 lead sources, and it reads today. An outbox full of invented IDs has to be rewritten before it can ever be replayed, which turns a deferred write into a rebuild.

"Can't the agent just send through Entrata?" Checked: no, and that's the industry norm

Entrata has guest SMS, but it's a dashboard tool with no API to send outbound programmatically. Every major AI leasing agent (EliseAI, Funnel, Knock) sends on its own number and logs back to Entrata. The only thing that sends natively through Entrata is Entrata's own ELI+ AI (because it is Entrata, a black box we'd rather not hand the conversation to, it costs us the accountability transcript + speed tuning + data).

Single point of contact, the right way: the agent owns one number, and we forward the property's published number to it (exactly what EliseAI does). The guest sees one consistent number and one thread; the on-site team works from the logged notes.

The one weakness to manage: split-brain

If LAG separately messages the same prospect through Entrata's own texting/email tools, that message is in Entrata but not our system, and the thread fragments. Fix is policy, not tech: the agent owns the conversation channel; LAG works from the logged notes and takes the tour. Route the Apartments.com lead email only to the agent for first-touch so two people never reply to the same renter. Note this bites harder while note logging is deferred, since there are no logged notes in Entrata yet to work from.

What the lead loop actually has: getLeads and getLeadPickLists read today. sendLeads and updateLeads are permissioned and banned. Everything else on the old wish list is off the key and stays off on purpose: getMitsLeads and sendMitsLeads were never granted, getLeadEvents was left out deliberately because getLeads takes eventDateFrom / eventDateTo / eventTypeIds and returns the matching events inline, and the whole customers group is absent, which costs us nothing: resident data comes from getLeases and the customer IDs come from getMitsLeases. One caveat still unconfirmed: the contact-form message body and unit-of-interest may not pass through the structured guest card, only name/email/phone/move-date are documented.

Two paths, and we'll use both

These solve different problems, and the research cleared up which does what:

Net: the API question is settled. What's left with Liberty is the ILS syndication flip, the per-property application link, property scope for Aspen and Summerset, and two write asks added 2026-08-05 that have never been sent: sendWorkOrders on NPI-owned properties and sendLeaseActivities for logging Foothold's own resident touches. Those two go out in one email, and a grant on either still doesn't authorize a call.

Access, scope, and limits

Access: settled. One key, one property, read-only

The API user lives on Liberty's Entrata account (org libertyassetgroup, id 19530). The App Developer Interface Agreement was signed 2026-07-20 at $0 for Year 1, access cleared 2026-07-31, the key was widened again 2026-08-03, and every one of the 28 services on it was probed by 2026-08-05. No cost surprise ever surfaced.

Auth is an X-Api-Key header plus a matching block in the request body, both required, and everything is POST. Entrata whitelisted the NPI VPS static IP 5.78.139.104, and all traffic originates there. No laptop, no Worker, no exceptions. Cost is worth revisiting only if Liberty extends scope, since that's when a per-property line could show up on their side.

Adding Aspen and Summerset

Neither is on the key, and neither is in Liberty's Entrata yet. Aspen closes 8/31. Until one of them lands there is no property we can legitimately run the resident-side engines against, because Owyhee Park is Liberty's building, not ours.

Extending the key is the same conversation that created it: Liberty asks their Entrata CSM to add the property to the existing API user. Two things ride along with it. Webhook scope is bound to the same property grant, so we don't build a second integration for it. And property scope is a security control on our side too: the key can already see 98 Liberty properties through getWebsites, including buildings other owners own, so the code enforces a property-ID allowlist next to the read-method allowlist, at the same choke point. We do not probe another owner's building.

Rate limits and the traps that cost hours

Limits are per method, not one global cap, and they're generous where it matters: availability at 600/minute, leases at 1,072/minute. The three that actually bind: getStatus at 6 per minute (so it can't be a heartbeat), getWorkOrderPickLists at 1,000 per day, and getMarketingPreferencePickList at 60 per hour. The two pick lists get cached once a day per property, never called per lead or per ticket. Poll availability on a cadence and serve the site from cache, never call Entrata on a page view.

Four things that will bite the client: calls must be serial, never concurrent, one gate for the whole account. Set an explicit User-Agent, Cloudflare's edge 403s a default one with a non-JSON body reading error code: 1010, which reads exactly like a revoked key and isn't. Window caps are per method (730 days on leads, 7 on the calendar), so measure each one. And every date and time is Mountain, which is a one-day error on move-in dates near midnight if parsed lazily. Entrata's maintenance window is Tuesday late night into Wednesday, weekly: back off, serve last-good cache, alert only past 2 hours stale.

Bottom line: the connection exists and reads clean, against one property that isn't ours. The work now is the poller, the cache, the consent gate, and an outbox that holds every write with real IDs in it until you sign one off. owner@footholdboise.com goes straight to Brent.