# Foothold design scope: intent vs. API reality

**Intent captured from Brent 2026-07-31. API findings from the 2026-07-31, 2026-08-03, and
2026-08-05 live read-only probes, the full vendor OpenAPI spec, and the vendored webhooks doc.
Decisions added 2026-08-05, 2026-08-10, and 2026-08-13 (the DECISIONS.md session: all twenty open
decisions answered; the design is locked).**

> ## 📌 UPDATED 2026-08-13: ALL TWENTY OPEN DECISIONS ANSWERED. THE DESIGN IS LOCKED. Read this first.
>
> **On 2026-08-13 Brent answered all twenty open decisions in `DECISIONS.md` at the repo root.** The
> answers sit on that file's `→ ANSWER:` lines and are filed through this document as
> **DECIDED 2026-08-13 (Brent, DECISIONS.md session)** entries. **Locked does not mean buildable:**
> nine build-spec areas (outbox and replay, the Supabase schema, retry and backoff, data retention,
> secrets rotation, the write-allowlist config format, resident-verification mechanics, the
> escalation trigger, maintenance-window behavior) get written next, before any code.
>
> Headlines for a reader of this document: Liberty's surfaces are **Entrata itself plus a five-line
> weekly digest email, with NO standalone Foothold page** (#1, §1.4); the first write sign-off names
> **`sendLeads` + `updateLeads` at go-live**, with work orders and lease activities on a second
> signature (#2, §4.4); the guest-card trigger is **AI judgment behind the hard minimum-info gate**
> (#3, §1.2); FlightDeck is **internal-only today, built toward self-serve software** (#4, §3.4);
> lead-event logging builds against the **numeric `typeId`**, transcript and channel in `comments`,
> with the `typeId` verification itself still open (#5, §1.4, Tier 3 #8); the maintenance KPI gets
> **no target and goes on the scoreboard** (#13, §3.1); **"watch the scoreboard, not the plays" is
> promoted into the Constitution** (#14, Tier 3 #19); webhooks get **provisioned, bundled into the
> Aspen/Summerset onboarding Liberty already has in motion** (#16 and #18, §4.6); and the
> `sendWorkOrders` / `sendLeaseActivities` email is **TABLED, not sent** (#19 and #20, asks #18/#19).
>
> 🚫 **Constitution rule 2 is unchanged, and none of the twenty answers authorises a write today.**

> ## 📌 UPDATED 2026-08-10: TWO DECISIONS FROM BRENT. Both close open questions. Read these first.
>
> **DECIDED. Neither authorises a write, neither needs a new permission, and both run on reads already
> on the key.** Do not reopen either without Brent.
>
> 1. 📊 **Maintenance responsiveness is an AGGREGATE KPI, and it serves two purposes.** Brent chose
>    aggregate over per-ticket policing, so **"watch the scoreboard, not the plays" is AFFIRMED, not
>    overturned.** `lead-comms-spec.html` §5 step 3 stands exactly as written, and it is now **a
>    decision Brent has actually made** rather than an unattributed design-intent line. ⚠️ **What is
>    new: a maintenance responsiveness metric SHOULD now be built.** The design previously had no
>    resolution-time metric at all. Reconcile that with the "no nightly SLA-policing" line plainly, and
>    do not delete that line: **measuring the aggregate is not policing; alerting on or chasing an
>    individual ticket still is.** ⚠️ **It is dual-purpose, and that shapes how it must be built.**
>    Brent's reasoning: *"If that KPI is genuinely good we can use it in marketing, and if it is bad
>    then it is an accountability metric for LAG."* Good numbers become marketing proof for the
>    Foothold brand thesis; bad numbers become an accountability metric in the Liberty relationship.
>    **Both uses demand the same thing: one definition, written down once, computed the same way every
>    time, and never cherry-picked by window. A number used in both directions cannot be redefined when
>    it is inconvenient.** Computed read-only from `getWorkOrders` (`completedOn` **OBSERVED**
>    2026-08-05, `createdOn` **SPEC**, plus the `createdOnFromDate` / `createdOnToDate` /
>    `completedOnFromDate` / `completedOnToDate` request filters). **No new permission, no rule 2
>    exposure, and nothing to ask Liberty for.** Build cautions and the open items are in §3.1.
> 2. 💬 **The resident follow-up is an AI-generated personal message about their specific issue.**
>    Brent, verbatim: *"an AI generated personal message about their specific issue, 'Did your back door
>    get fixed?'"* **It is NOT the survey `lead-comms-spec.html` §4 cut, and that cut STAYS cut.** It is
>    the lighter personal check-in, and it is precisely the thing that **survives the objection that
>    killed the survey**, because it is specific, personal, and from the ownership number rather than a
>    form. **The posture question is resolved too, and it does not conflict with "watch the scoreboard":
>    the message goes to the RESIDENT, not to Liberty.** Nothing per-ticket is sent to, demanded of, or
>    logged against Liberty. The posture is "we care about you," not "we're auditing Liberty."
>    **Per-ticket outbound to a resident and per-ticket policing of a vendor are different acts.** It
>    reads `maintenanceProblem`, `maintenanceLocation` and `maintenanceDescription` off `getWorkOrders`,
>    all readable today. 🚫 **Scope: NPI-owned properties once Foothold is live on them. NOT Owyhee
>    Park**, which is Liberty's building with Liberty's residents. Everything measured so far was
>    measured at Owyhee, and **reading their tickets does not licence messaging their residents.**
>    Filters, the new don't-invent-detail rule, and what stays open are in §3.2.
>
> **This closes Tier 3 open question #7 and §3.2's GAP items 2 and 3.** It opens three new Tier 3
> items (#20, #21, #22) and it moves nothing on rule 2.

> ## 📌 UPDATED 2026-08-05: THREE DECISIONS FROM BRENT. Read these before any block below.
>
> These are **decisions**, not findings. They close two long-standing GAPs and add a fourth item to the
> ask list. **None of them authorises a write today.**
>
> 1. 🔓 **Rule 2 will be lifted, but only on NPI-OWNED properties, and only at go-live. It is NOT
>    lifted today.** The read-only rule comes off **NPI-owned** properties when Foothold goes live on
>    them (Aspen, then Summerset). **Three conditions must ALL hold before a single write is issued:
>    (1) NPI owns the property, (2) Foothold is live on it, and (3) Brent has signed off in writing
>    naming that property and those methods.** ⚠️ **Owyhee Park is NOT covered and is not expected to
>    be.** It is Liberty's building, not NPI's, and a property NPI does not own stays read-only
>    regardless of permissions. Everything in this document was measured at Owyhee, so **a working
>    probe is not a licence to write.** ⚠️ **The deny-by-default allowlist does not disappear, it
>    becomes PER-PROPERTY**: same single choke point, same fail-safe, but a named property can carry a
>    named set of permitted write methods rather than the list being empty everywhere forever. **A
>    missing, unparseable or unrecognised property config still means read-only. Build it per-property
>    from the first line of client code**, because retrofitting a per-property gate onto a global bar
>    is exactly where the accident happens. Everything else about rule 2 stands: `sendLeads` and
>    `updateLeads` are permissioned and still never called, Brent declined to have them removed, and
>    there is no vendor-side guard. §4.4.
> 2. 🔧 **On NPI-owned properties, once live, the Foothold agent SHOULD file the service ticket
>    itself, after verifying the resident.** This **reverses** the "Foothold must never be the intake"
>    recommendation this document carried. **Two hard gates come first:** (a) **`sendWorkOrders` is
>    NOT on the key and is a new ask to Liberty that has never been made**, and (b) rule 2 must be
>    lifted for that property per decision 1. **The resident MUST be verified before a ticket is
>    filed**, because an unverified ticket sends a real technician to a real unit, and **fail closed:
>    no match means hand off to a human.** ⚠️ **Until both gates clear, and permanently at Owyhee
>    Park, point residents at Entrata's Resident Portal and do not take the request.** §3.2.
> 3. 🔄 **`sendLeaseActivities` IS a near-term ask to Liberty. Brent deferred it earlier on 2026-08-05
>    and REVERSED that the same day.** Logging Foothold's own touches (birthday card sent, pet card
>    sent, feedback received) into Entrata goes on the ask list, in the **same conversation** as
>    `sendWorkOrders`: that one is the larger and more consequential write grant, so bundling the
>    low-blast-radius activity note into it is one round trip instead of two, and the standing lesson
>    here is one email carrying everything, not a second email chasing the first. **Frame it to Liberty
>    the way it actually helps them:** it puts Foothold's touches in their system **instead of their
>    inbox**, so it reduces their email volume rather than adding to it. ⚠️ **The grant is not the
>    authorization.** It is a **write**, so rule 2 governs it exactly like the others: NPI-owned, live,
>    and signed off in writing naming that property and that method, and **at Owyhee Park it stays
>    unused even if granted**, because Owyhee is not covered by the lift. The findings stay on the
>    record: it is the only method in the 128 that appends an activity **to a resident / lease record**,
>    it is not on the key, and it covers **resident / lease-level touches only**. ⚠️ **CORRECTED
>    2026-08-10: the clause that used to sit here, "lead-level events can NEVER be logged, because no
>    lead-event write method exists anywhere in the spec," was WRONG and is withdrawn.** `updateLeads`
>    r2 and `sendLeads` r1 both carry an `events` array with a free-text `comments` field and both are
>    on the key. **The ask stands unchanged** (it is for resident-level touches, which is exactly what it
>    is for), **only this piece of its stated reasoning was wrong.** §1.4, §2.6, asks #19.
>
> **The near-term asks to Liberty are therefore FOUR: Aspen + Summerset onto the key** (#7, the largest
> unowned dependency before the 8/31 close), **the per-property application link** (#17, asked 8/03,
> unanswered), **`sendWorkOrders` for owned properties** (#18, new), and **`sendLeaseActivities`**
> (#19, new, and it rides in the same email as #18).

> **UPDATED 2026-08-05 (third pass).** A DOB historical sweep across the full lead history, plus the
> last few methods on the key that had never been called. **Every method on the key has now been
> probed read-only. There is nothing left unprobed.** Five things changed:
>
> 1. ⚠️ **CORRECTION, and it is the important one: `getLeads` `birthDate` was never "populated 122 of
>    122."** Entrata **omits null fields** from its responses, so that count only ever measured
>    records where the key already existed. It was circular and it measured nothing. **The full sweep,
>    2021-01-01 through 2026-08-05: 787 distinct lead-customers, 286 of them carrying a `birthDate`,
>    which is 36%.** Every "122 of 122" in this document is corrected in place.
> 2. 🎂 **DOB COVERAGE IS MEASURED, and it is 65% of current residents.** This was the explicitly
>    flagged open unknown and it now has a number. The rent roll holds **103 customer records across
>    52 leases, 88 of them `Current`. 70 of the 88 appear in `getLeads`; 57 of the 88 carry a
>    `birthDate`.** The 31-resident gap splits two ways: **18 never appear in `getLeads` at all**, and
>    **13 appear but have no birth date on the lead record.** Consequence: **the Welcome Profile is a
>    REQUIRED component of the design, not an optional fallback**, because `getLeads` covers about two
>    thirds of the roll and the remaining third has to come from us. ⚠️ **Do not describe birthdays as
>    solved.** §2.2.
> 3. ✅ **The lead-to-resident join is PROVEN, not assumed.** `getLeads` `customerId` and `getLeases`
>    customer `id` are the **same namespace**: 70 of 88 current residents matched on it. The earlier
>    concern that they might be different ID spaces is **closed.** §2.2.
> 4. ✅ **Backfill is cheap, and window caps are per method.** `getLeads` accepts a **730-day**
>    window, so the entire 5.5-year history swept in **3 calls**. The earliest `moveInDate` on the
>    roll is **2022-06-16**. Contrast `getCalendarAvailability`'s **7-day** cap (§1.6): ⚠️ **a window
>    cap is a per-method fact and never generalizes. Measure each one.**
> 5. 🚨 **NEW BUILD-CRITICAL TRAP: Entrata's edge is Cloudflare, and it 403s a default User-Agent.**
>    Python `urllib` on its default UA returned **HTTP 403 with the body `error code: 1010`**, a
>    Cloudflare browser-signature ban, **not an Entrata JSON error and not a permission failure.** The
>    identical call through `curl` worked. **The real client MUST set an explicit User-Agent**, and it
>    must treat a **non-JSON 403 body as an edge block**, distinct from a 400 / 1403 permission
>    denial. §4.2.
>
> **Rate limits are complete too** (§4.2), which closes the standing note that the work-order methods
> had no measured limit, and it turns out **there are two constrained methods, not one**:
> `getWorkOrderPickLists` holds the tightest **daily** bucket (1,000/day) and
> `getMarketingPreferencePickList` the tightest **hourly** one (60/hr, plus 10/min).
> **Cache both.** (The tightest **per-minute** bucket on the key belongs to neither of them: it is
> `getStatus` at **6/min**, which is why `getStatus` cannot be a heartbeat.)
>
> **Constitution rule 2 is unchanged.** Every call in this pass was a **READ**, issued from the VPS.
> **No writes were made and none are authorized.**

> **UPDATED 2026-08-05 (second pass).** A follow-up round of read-only probes, plus a document-side
> scan of Aspen's executed leases. Three things changed:
>
> 1. 🚨 **NEW BINDING DATA RULE FROM BRENT: never ingest, persist, or log an SSN.** NPI has no use
>    for it and does not want the liability. SSN / `taxId` sits in the **response** schemas of
>    `getMitsCollections` r1, `getVendors` r1, and **`getMitsLeases` r1 (`SocialSecurity`), which is
>    on our key.** The 2026-08-05 live probe of `getMitsLeases` returned **no SSN keys**, so Liberty
>    leaves the field empty and there is no exposure today, but the field could start populating at
>    any time. **The single request choke point that already enforces the read-method allowlist must
>    also strip `SocialSecurity` / `SSN` / `taxId` on read, before anything is persisted, logged, or
>    passed to an AI model.** Fail-safe, at the transport layer, never a downstream cleanup pass.
>    This and §2.2's month-and-day-only birth-date rule are the **two standing PII-minimization
>    rules**, and they govern lease-document and DD ingestion too, not just the API.
> 2. **Pet data is real, rich, and it unblocks the pet-gift engine.** `getLeases` r1 carries **34
>    individual pet records across 30 of 52 leases**, each with `name`, `petType`, `breed`, `color`,
>    `gender`, `age`, `weight`, `moveInDate`, `customerId`, and `isAssistanceAnimal`. ⚠️
>    **`isAssistanceAnimal` is load-bearing: assistance and service animals are NOT pets under fair
>    housing, so they get filtered out of the pet-gift program and out of any pet-themed
>    marketing.** §2.3.
> 3. **Aspen's 44 executed leases confirm the DOB finding from the document side.** All 44 were
>    text-extracted (7.4M characters, no OCR needed): **zero occurrences of "date of birth" or
>    "birth."** An executed lease records the *result* of screening, never the applicant's DOB, so
>    **`getLeaseDocuments` should not be pursued for DOB at any property.** §2.2.
>
> **Constitution rule 2 is unchanged.** Everything above came off a read or off documents we already
> hold. **No writes were made and none are authorized.**

> **UPDATED 2026-08-05.** The five methods that were permissioned but unprobed have all been
> **called read-only, and all five returned HTTP 200 and real data.** Nothing in this document
> carries the label "PERMISSIONED, UNPROBED" anymore. Four findings changed the design:
>
> 1. **Birth dates are readable today, and not where we expected.** `getMitsLeases` works and
>    returns **zero** birth dates. `getLeads` r1 returns `birthDate`. ⚠️ **CORRECTED by the third
>    pass, same day: the "populated, 122 of 122" figure recorded here was an artifact of Entrata
>    omitting null fields, and it was wrong. The measured numbers are 36% of lead-customers (286 of
>    787) and 65% of current residents (57 of 88).** DOB is captured at the **lead / application**
>    stage and **never reaches the lease or resident record.** §2.2 now runs off `getLeads`, joined to
>    the resident on customer ID, and ⚠️ **stores month and day only, never the year.**
> 2. **The tour calendar is real configuration, not an empty shell.** `getCalendarAvailability`
>    returns 30-minute appointments, a 120-minute minimum lead time, agent-guided **Mon-Fri
>    08:00-16:00 MST**, and **self-guided tours all seven days, 06:00-18:00 MST, limit 1**, which
>    matches Alex's lock-box answer exactly. ⚠️ **The live window cap is 7 days, not the 60 the spec
>    implies**; 8 or more returns HTTP 400, code 308. §1.6.
> 3. **Consent is a real gate, and it is now readable.** `getMarketingPreferences` returns `optIn`
>    per consent type per channel, and the `customerIds` blocker is solved: the IDs come off
>    `getMitsLeases` (`IDType: "Customer ID"`). **Nothing messages a resident or a lead until this
>    is checked.** §1.3, §1.8.
> 4. **`getWorkOrderPickLists` carries Liberty's staff roster** (`CompanyEmployees`, 12 people), so
>    work-order routing to a named human is expressible on reads alone. §3.1.
>
> Rewritten for the new state: §1.3, §1.6, §1.8, §2.2, §2.5, §3.1, §3.3, §4.2, §4.3, the asks list,
> and the open questions.
>
> **Constitution rule 2 is unchanged.** Every one of these was a read. **No writes were made and
> none are authorized.** More reachable surface is a reason to be more careful, not less.

> **UPDATED 2026-08-03.** Liberty granted a second round of permissions. The key now carries **28
> services, up from 22**. Six new methods landed, **all reads, exactly the six asked for on
> 2026-07-31**, and **no new writes**. `getWorkOrders` is not just permissioned but **verified
> returning real production work orders**. Alex Costa also answered the two operating questions
> (tour booking, human escalation). Rewritten for the new state: §1.5, §1.6, §1.7, §1.8, §2.2, §2.5,
> §3.1, §3.2, §3.3, §4.2, §4.3, §4.4, §4.6, the asks list, and the open questions.
>
> **Constitution rule 2 is unchanged.** More reachable surface is a reason to be more careful, not
> less.

> ## ⚠️ THE DESIGN IS NOT FINALIZED
>
> **STATUS CHANGE 2026-08-13: every open decision is now ANSWERED (DECISIONS.md session) and the
> design is LOCKED.** Locked is not buildable: the nine build-spec areas come next, before any code.
> The heading above is kept for the trail; the description below of how this document works is
> unchanged.
>
> This document does two jobs and only two jobs: it records **what Brent wants**, and it records
> **what the Entrata API can actually do**. Where those collide, it names the collision and lays
> out the options, then stops. **It does not decide architecture on its own.** When Brent closes a
> gap, the GAP block becomes a DECIDED block recording his call and the date; that's the only way a
> decision lands here. Nothing here is a build instruction. The binding decisions live in [`system-design.html`](../system-design.html)
> (§2 Constitution, §4 Entrata protocol). If this doc and the Constitution ever disagree, the
> Constitution wins and this doc is what needs fixing.
>
> A future agent reading this should treat every INTENT block as settled, every DECIDED block as
> closed, and every GAP block as open.

---

## 0. How to read this

Every capability below gets the same three blocks, in the same order:

| Block | What it is | Who owns it |
|---|---|---|
| **INTENT** (blockquote) | Brent's stated design intent, 2026-07-31. A **decision**. Recorded faithfully, not softened, not reinterpreted. | Brent. Do not edit without him. |
| **REALITY** | What the API can do, from the probe and the spec. A **constraint**, not an opinion. | The vendor. Verified where it says verified. |
| **GAP** | Where 1 and 2 collide, plus the options. **No decision is made here.** | Open. Brent decides. |
| **DECIDED** | A GAP Brent has since closed. Says what the decision is, what's left as config or as a question for someone else, and when he made it. | Brent. Closed. Do not reopen without him. |

If a capability has no GAP block, intent and reality agree and there is nothing to resolve.

**Provenance labels used in REALITY blocks:**

- **VERIFIED** = came off a real response from Liberty's production org, on the 2026-07-31, the
  2026-08-03, or the 2026-08-05 read-only probe.
- **SPEC** = read out of the vendor OpenAPI spec, not exercised live.
- **UNPROBED** = the method exists but we have never called it, so its **behavior** is unknown. ⚠️ Its
  **permission** never is: the `getAccessibleServices` roster settles that for all 128 methods without
  a probe, so "UNPROBED" must never be read as "might be permissioned."
- **PERMISSIONED, UNPROBED** = confirmed on the key by `getAccessibleServices`, never called. The
  permission is a fact; the behavior is not. **Never state what one of these returns.** ✅ **As of
  2026-08-05 nothing in this document carries this label**: all five methods that did were probed
  read-only and all five work. ✅ **Extended by the third pass the same day: every method on the key
  has now been called, including the oauth and appinfo plumbing, so there is nothing left to probe at
  all** (§4.2 records what the plumbing returns). The definition stays for the next grant.
- **BLOCKED (1403)** = called it, the API user is not permissioned for it.

**Companion docs, do not duplicate them here:**
`reference/entrata-touchpoints.md` (🟢 **the current-state API reference, and it wins on any API fact
this document contradicts**) ·
`reference/entrata-api-context.md` (real data shapes, traps, PII map) ·
`reference/entrata-method-index.md` (per-method signature and permission status) ·
`reference/entrata-webhooks-2026-03-04.md` (verbatim vendor webhook doc) ·
`reference/entrata-openapi-2026-07-28.yaml` (the full spec).

### The three modules in one paragraph

**Module 1 (front-end leasing)** is the demand side: leads land in GHL from the ebook and the
waitlist, and the moment a prospect names a property they become an Entrata guest card, after
which every 1:1 message routes through the Foothold agent and mirrors into Entrata. **Module 2
(physical touches)** is the retention side: Handwrytten cards and gift cards fired off resident
milestones (move-in, birthday, pre-renewal, tenure, referral, review ask). **Module 3 (work orders
and FlightDeck)** is the asset-management side: watch work-order completion, close the loop with
the resident by text, and expose the whole Entrata integration to FlightDeck as the beta of a
sellable API-access product. Module 1's writes are entirely blocked today, **Module 2 is mostly
buildable, birthdays now have a verified source, `getLeads`, not the lease record, though it covers
only 65% of current residents so the Welcome Profile is a required second source** (§2.2), **and so
do pets, `getLeases`, 34 records carrying the pet's name** (§2.3), and
**Module 3's core data source landed 2026-08-03**: `getWorkOrders` is on the key and verified
returning real production work orders. Two Module 1 pieces do **not** wait on the writes: the application handoff (§1.7, the agent hands over Liberty's hosted
ProspectPortal URL) and the leg of escalation that actually reaches a human (§1.5, an out-of-band
notification, with the Entrata note as the record).

---

# Module 1: front-end leasing and inbound demand

## 1.1 Top-of-funnel capture (ebook, waitlist) into GHL

> **INTENT.** Leads are captured by downloading the ebook or signing up for the waitlist. Those go
> into GHL.

**REALITY.** No Entrata involvement, so no constraint. This is already built on the public site:
`foothold-web` `src/pages/api/waitlist.ts` upserts to GHL through `src/lib/ghl.ts`, write-only,
with tags (`foothold-guide-rent`, `foothold-sms-optin`) and `?ref=<email>` referral capture. GHL is
the lead store. Nothing about the Entrata findings changes this.

**No gap.**

---

## 1.2 The trigger: expressed interest in a property creates the guest card

> **INTENT.** Leads are **NOT** entered as an Entrata guest card until they express interest in a
> particular property. That's the trigger. Once they express interest in a property, they become a
> guest card in Entrata.

**REALITY.**

| Fact | Detail |
|---|---|
| Method | `sendLeads` r1, group `leads`. **This is a WRITE.** |
| Permissioned? | **Yes.** VERIFIED on the key 2026-07-31. Liberty enabled it. |
| Allowed? | **No.** Constitution rule 2 bars every write against a live production property until Brent signs off in writing, naming the property and the methods. Having the permission is not authorization. |
| Required params | `propertyId` + `prospects[]`. Each prospect requires `createdDate` + `customers`. |
| Dedupe path | `getLeads` filtered by email is a read and works today, so the "does a card already exist" half of the loop is exercisable now. |
| Lead source | ⚠️ **There is no Foothold lead source in Liberty's pick list.** The 17 sources on the property are Liberty's own. `leadSourceId 67822 "Website"` is Liberty's site, not ours. |
| Lead status | ⚠️ **Not settable.** No `leadStatusId` / `statusId` param on `sendLeads` r1, `updateLeads` r1, or `updateLeads` r2. The 62 statuses are read-only. Entrata assigns status; we cannot. |
| Attribution field | `googleClickId` and `googleClientId` exist on the customer object (VERIFIED on `getLeads`), so the click-to-lease loop is expressible once writes are on. |

**GAP.**

1. **The trigger logic is fine, the write is not.** Brent's rule (property-specific interest is the
   hinge) is cleanly implementable and it's a good rule. The problem is purely that the card cannot
   reach Entrata today. Every guest card queues in the outbox unsent. Liberty sees nothing.
2. **"Expressed interest in a particular property" needs a machine definition.** Named the property
   in chat? Clicked a specific unit? Asked about rent at that building? Submitted the property
   inquiry form? The constitution already has a **minimum-info gate** (first name + one contact
   method) which is a separate and narrower test. Options: property-named-in-conversation, explicit
   form submit only, or agent-judged intent. Not decided. ✅ **DECIDED 2026-08-13 (Brent,
   DECISIONS.md session): AI judgment is fine, and the minimum-info gate is HARD: (phone OR email)
   AND at least a first name. No guest card without a way to follow up.** The agent judges the
   interest; the gate is the non-negotiable floor. Closes Tier 2 #6.
3. **Attribution has to be reconciled, not written, and that stays true even after sign-off** until
   Liberty adds a Foothold lead source. Near term, tag in GHL and reconcile against `getLeads` by
   email and phone. Build the reconciler regardless; it becomes the audit path later. 📌 **DECIDED
   2026-08-13 (Brent, DECISIONS.md session): build the reconciler regardless, confirmed. The
   "Foothold lead source" ask (asks #6) bundles into the tabled #18/#19 email whenever it goes;
   nothing is sent now.**

---

## 1.3 Chat, email, and SMS conversation with the prospect

> **INTENT.** Once they express interest in a property, the Foothold chatbot and email bot can
> communicate with them. Texting will be set up too. Website chat, email, and texting all
> communicate with Foothold.

**REALITY. Entrata sends nothing. Ever.**

**There is no `sendSms`, `sendMessage`, `sendEmail`, chat, or push method anywhere in the 128
methods.** All 18 groups enumerated. This is not a permissions issue and it will not change with a
checkbox.

| Capability | Entrata |
|---|---|
| Send an SMS | **Does not exist** |
| Send an email | **Does not exist** |
| Chat / push | **Does not exist** |
| Log that a communication happened | Yes, as a lead event. **WRITE.** |
| Read whether a contact consented to SMS/email | Yes, `getMarketingPreferences`. Read. **VERIFIED 2026-08-05.** |

**Entrata is a system of record, never a transport.** Transport is Twilio (SMS), whatever mail
sender the agent uses (email), and the site widget (chat). That is already the design's assumption,
and the spec confirms there was never another option.

**SMS consent is readable but not writable. The read is VERIFIED 2026-08-05.**
`getMarketingPreferences` (group `communications`; requires `customerIds` max 100 + `propertyId` +
`recipientTypeId` where 1=LEAD 2=RESIDENT) was called read-only with `recipientTypeId=2` and
returned HTTP 200 with `optIn` **per consent type per channel**. `communicationChannel` is email /
phone / postal_mail / sms, as the spec said. **The `customerIds` chicken-and-egg is solved:** the
IDs come off `getMitsLeases`, which returns them as `IDType: "Customer ID"` (§2.2). So the whole
loop (enumerate residents, fetch their consent, decide whether to send) runs on reads we have.

⚠️ **VERIFIED 2026-08-05 (third pass): it works on the LEAD side too, and a customer can hold both
sets at once.** `recipientTypeId=1` returns HTTP 200 the same way `recipientTypeId=2` does, and **the
same customer can carry BOTH a LEAD preference set and a RESIDENT preference set simultaneously.**
So the consent check has to say which relationship it is checking. A prospect who later signs a lease
does not have one record that changes type; they have two. **Check the set that matches the message
being sent, and never let one imply the other.**

`getMarketingPreferencePickList` is **VERIFIED 2026-08-05** too: 24 rows, recipient types LEAD(1)
and RESIDENT(2), the four channels. ⚠️ **It carries the tightest hourly bucket on the whole key:
1,500/day, 60/hour, 10/minute.** It is a vocabulary, it barely changes, and it must be
**cached**, not called per message. ⚠️ **Corrected 2026-08-05 (third pass): it is not the tightest
bucket outright.** `getWorkOrderPickLists` runs **1,000/day, 500/hour, 60/minute**, so it holds the
**tightest DAILY** bucket on the key, and `getStatus` at **6/minute** holds the tightest per-minute
one. There are two constrained vocabularies, they are constrained
differently, and **both get cached** (§4.2).

**This is a real gate, not a nicety.** Consent state is now checkable before any 1:1 message to a
resident or a lead, and the design should treat it as a precondition on the send path rather than a
reporting curiosity.

What is still true: there is **no** `updateMarketingPreferences`, and `sendLeads`'s own
`marketingPreferences` block carries email, phone, and mail opt-ins but **no SMS sibling**. So SMS
consent has to be *captured* and held in GHL, and A2P/TCPA compliance is entirely ours. Entrata
tells us who has opted out; it will never let us record that someone opted in.

**No gap on transport.** The gap is on logging, next.

---

## 1.4 Logging every communication into Entrata

> **INTENT.** All those communications get logged in Entrata.

This is Constitution rule 4 restated by Brent, and it's the core of the PM-visibility promise:
Liberty sees everything Foothold says to a renter.

**REALITY.**

| Fact | Detail |
|---|---|
| Method | `updateLeads` r2, `events` array. **This is a WRITE.** |
| Allowed? | **No.** Rule 2. Deferred to the outbox, not waived. |
| Event type vocabulary, r2 | ⚠️ The `type` enum has **no `Note`, no `IncomingText`, no `OutgoingText`**. It carries only: `CallFromProspect`, `CallToProspect`, `Appointment`, `Tour`, `EmailToProspect`, `EmailFromProspect`. |
| Richer vocabulary | Exists on `sendLeads` r1 (no enum constraint) and `sendMitsLeads` r1. |
| Likely path for a text | Pass a numeric `typeId` pulled from `getLeadPickLists` (155 event types, VERIFIED). **UNVERIFIED and untestable without issuing a write.** |
| Time format trap | `sendLeads` r1 uses `"14:00:00"`, `updateLeads` r1/r2 use `"02:00pm"`, `getLeadEvents` returns `"10:00:00"`. Three formats, same concept. |
| `getLeadEvents` | **NOT permissioned.** Only `getLeads` and `getLeadPickLists` are on the key. |
| Compensating read | ⚠️ **Useful discovery:** `getLeads` itself accepts `eventDateFrom`, `eventDateTo`, and `eventTypeIds` (max 5). So lead-event deltas are pollable on a method we already have. `getLeadEvents` being blocked hurts less than it looks. |

**GAP. This is the sharpest collision in Module 1.**

1. **Today, zero communications reach Entrata.** Not "some." Zero. Everything queues to the local
   outbox. Brent's stated intent and rule 4's promise are both entirely deferred, which means
   **Liberty currently has no visibility into any Foothold touch.** Two ways out and they are not
   mutually exclusive: (a) Brent signs off writes on a specific property naming
   `updateLeads`, or (b) an explicit interim PM-visibility mechanism (a daily digest email to the
   Liberty PM, say) carries the promise until then. Right now neither exists, so the promise is
   silently unmet. Not decided. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): both ways
   out, and neither is a standalone page.** Brent's principle: **live inside Liberty's ecosystem,
   never make them open another system.** Liberty's surfaces are (a) **Entrata itself**, every
   Foothold AI conversation logged onto the lead's own record so a leasing agent who pulls up the
   guest card sees the history right there (`updateLeads` sits inside the first go-live signature,
   §4.4), and (b) a **five-line weekly digest email**: touches made, leads worked, tenants missing
   a DOB, anything needing their action. **No read-only Liberty-facing Foothold page.**
   Resident-side touches log to Foothold's own store and surface in the digest until
   `sendLeaseActivities` is granted (that ask is tabled, asks #19).
2. **Even after sign-off, texts and chat messages may have no correct native event type.** The r2
   enum has nothing for a text or a chat message. The numeric-`typeId` path is the likely answer and
   it is unverified. Options: use `sendLeads` r1 for events instead of `updateLeads` r2 (no enum
   there), map texts onto the closest existing type and eat the imprecision, or ask Liberty to read
   the real event-type IDs off their config. **Do not learn this by issuing a test write.**
3. **Pin the ID vocabulary now.** `getLeadPickLists` is readable today. An outbox full of intents
   built against invented `eventTypeId` values has to be rewritten before it can ever replay. Build
   the payloads against the real IDs while reads are the only thing allowed. ✅ **DECIDED
   2026-08-13 (Brent, DECISIONS.md session), covering items 2 and 3: build against the numeric
   `typeId`, targeting the lead's Entrata event timeline.** Pull the 155 IDs from
   `getLeadPickLists` now, while reads are all that is allowed; put a one-line summary on top and
   the **full transcript and true channel in the free-text `comments`**; keep enum-mapping as the
   runtime fallback; and **never learn any of it by issuing a test write.** One unknown rides into
   build: `comments` has no documented length cap, so if a full transcript bounces at go-live, fall
   back to summary-only. ⚠️ **The `typeId` verification itself REMAINS open (Tier 3 #8): which
   numeric ID is right for a text or a chat cannot be verified without issuing a write, so the
   approach is decided and the value is not.**
4. ⚠️ **CORRECTED 2026-08-10. The item that stood here, "a LEAD-level touch can NEVER be logged into
   Entrata, because no lead-event write method exists anywhere in the 128-method spec," was WRONG and
   is withdrawn.** It contradicted the table directly above it, which has named `updateLeads` r2's
   `events` array as the method the whole time. **The spec was read directly on 2026-08-10:**
   `updateLeads-r2-request-schema` (`reference/entrata-openapi-2026-07-28.yaml` lines 11672-12023)
   carries an `events` array (11923) with `eventId` (11931), `typeId` (11935), `date` (11943), `type`
   (11947) and a free-text **`comments`** field (11988, "Event comments"). `sendLeads` r1 carries an
   `events` array too (10604). **Both methods are on the key.** ✅ **Supporting evidence that the
   vocabulary exists in Entrata's model:** `sendMitsLeads` r1's `EventType` enum includes `Note`,
   `IncomingText` and `OutgoingText` (11202-11204). Recorded as evidence, **not** as a recommendation to
   route anything through `sendMitsLeads`. 🚫 **What did NOT change, and read this before drawing any
   conclusion from the correction: appending a lead event is a WRITE.** Rule 2 governs it completely,
   under all three conditions, exactly as before. **A capability correction is not a permission
   change.** ⚠️ **The real limitation is narrower and it is items 2 and 3 above, not this one: the r2
   `type` enum has no text, chat, or note value**, so logging a text or a chat means passing a numeric
   `typeId` off `getLeadPickLists` (155 event types, readable today), which is **unverified and cannot
   be verified without issuing a write.** That is **Tier 3 #8, which is live again rather than moot.**
   The resident / lease side is genuinely different and that half of the old finding survives:
   `sendLeaseActivities` is the only method in the 128 that appends an activity to a **resident**
   record, and **Brent put it on the near-term ask list 2026-08-05**, reversing his own deferral from
   earlier the same day (§2.6, asks #19). ⚠️ **The ask itself is unaffected** (it is for resident-level
   touches, which is exactly what it is for), **only the "and nothing exists on the lead side either"
   part of its rationale was wrong.** It is still not permission to call the method: it covers
   **resident / lease-level touches only**, and rule 2 still gates the call.

---

## 1.5 Human handoff: notify Liberty out of band AND note it in Entrata

> **INTENT (updated by Brent, 2026-07-31).** If the prospect wants to talk to a human, escalation
> fires an **out-of-band notification that Liberty staff actually monitor** (an email to the
> property inbox, a Slack message, or whatever they watch), **and** Foothold writes a corresponding
> **note on the lead record in Entrata** so Liberty sees it in context and the record is complete.
> **Both legs, not either.** The escalation signal does not have to be an Entrata object. The
> prospect can also just be given Liberty's phone number to call, same as before.

**REALITY. The out-of-band leg has no Entrata dependency and is buildable today. The Entrata note
leg is a write, so it queues like every other logging leg.**

There is still no ticket object in the API, and that no longer matters, because the design doesn't
need one. Zero keys matching `task`, `follow`, `reminder`, `escalat`, or `notify` across all 63,848
lines of the spec. No work queue, no assignment object, no to-do.

| Construct | What it actually is | Role in the design |
|---|---|---|
| Email to the property inbox, Slack, or similar | Not an API feature at all. No Entrata dependency, no rule 2 exposure. | **The attention leg.** This is what actually reaches a human. Buildable today. |
| Lead event with `agentId` (r1) / `leasingAgentId` (r2) + comments | A **log entry on the guest card**, not a work-queue item. `updateLeads` r2 `events` array. **WRITE.** | **The record leg.** Queues to the outbox until per-property write sign-off, same as every other logging leg. |
| `sendLeaseActivities` r1 with a `SCHEDULED_FOLLOWUP` event | The only construct in the whole API with a future `scheduleDateTime` **and** a mandatory `leasingAgentId`. The closest thing to a real task. | **Unusable here.** Requires a `leaseId`. Residents only. ⚠️ **Changed 2026-08-05**: it is **not on the key** and it is now a **near-term ask** (§2.6, asks #19), but that ask is for the resident touch log, not for this escalation path, and a grant would still leave it barred by rule 2. |
| `sendWorkOrders` | Maintenance request against a unit. Not a callback request. | **Unusable here**, a prospect has no unit. Still **not on the key**: the `maintenance` group landed 2026-08-03 with its two reads and neither write. ⚠️ **Changed 2026-08-05**: it is now a **live ask for NPI-owned properties** (§3.2, asks #18), but that is the resident service-ticket path, not this escalation path, and it is barred by rule 2 until that property clears all three conditions. |

Also relevant: `getProperties` VERIFIED office hours are **M-F 9:00 to 5:00, no weekend coverage**.
An escalation "to a person" on a Friday night reaches nobody until Monday.

**DECIDED 2026-07-31. One question deferred, and it's a question for Liberty, not a design
decision.**

1. **Two legs, split responsibilities.** The out-of-band notification carries the action; the
   Entrata note carries the paper trail. Because the attention leg has no Entrata dependency,
   **escalation never sits in an outbox waiting on a permission.** The note leg queues under rule 2
   like every other write and replays on sign-off, which costs the record nothing but timing.
2. ✅ **ANSWERED by Alex 2026-08-03: email Liberty's direct leasing / manager team.** Asked
   2026-07-31 (Liberty asks #15), answered by email at 08:47 AM PT on 2026-08-03. **Email is the
   channel.** They also run Google / Breakroom chats, but they **prefer email.** Today's recipient is
   `adrienne@libertyassetgroup.com`. ⚠️ **Alex flagged that the team is being restructured, so treat
   the recipient as per-property config, not a constant.** Store it alongside the ProspectPortal URL
   in `property-data-capture.html`, re-verify it when a property is added or a reorg lands, and never
   hardcode an address into the escalation path. Brent's assumption (property inbox or Slack) is now
   replaced by a fact: a named human's inbox, subject to change.
3. **The "OR they get Liberty's phone number" half works today** with no API involvement at all.
   Still true, still free.
4. **Hours interact with the handoff.** Constitution rule 10 hands off at tour-booked or
   application-submitted. A Friday 10 PM handoff hands off to nobody. Either the agent holds the
   conversation or the handoff gets an explicit business-hours queue. Flagged in
   `system-design.html` §14, still not decided. That's a separate question from the escalation
   channel, which is settled. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): hybrid.** The
   agent **holds the conversation by default** after hours (offer the Friday-night lead a
   self-guided tour tomorrow morning); an emergency or an explicit "I want a person now" escalates
   **immediately**, with the hours stated plainly. **On an emergency the agent steps out of the
   loop at once and hands over the phone number ("please call this number now"), never a queued
   callback.** Closes Tier 3 #12.

---

## 1.6 Tour scheduling

> **INTENT.** The Foothold AI leasing agent needs to schedule tours.

**REALITY. The reads work and they return real configuration, which reverses what this section said
on 2026-08-03. The booking is still a write, and it still has traps.**

**Reads. `getCalendarAvailability` VERIFIED 2026-08-05**, called read-only, HTTP 200, real response:

| Method / version | Window | Returns |
|---|---|---|
| `getCalendarAvailability` r1 | 7 days | `appointmentLength`, `selfGuidedTourAppointmentLength`, `minLeadTimeTour`, per-day `availabilities` with `simultaneousAppointmentLimit` |
| `getCalendarAvailability` r2 | ⚠️ **7 days in practice, NOT the 60 the spec implies.** An 8-day-or-wider range returns **HTTP 400, error code 308.** Requires `tourType` enum (`AGENT_GUIDED` / `SELF_GUIDED` / `VIRTUAL_GUIDED`) | Slots as `tourDate` + `tourTime[]` strings, literally `"09:00 MST - 09:30 MST"` |
| `getProperties` (VERIFIED) | n/a | Office hours **and holiday hours** |

**What Owyhee's calendar actually holds, VERIFIED 2026-08-05:**

| Setting | Value |
|---|---|
| Appointment length | **30 minutes** |
| Minimum lead time | **120 minutes** |
| Agent-guided availability | **Mon-Fri, 08:00-16:00 MST** |
| Self-guided availability | **All seven days including weekends, 06:00-18:00 MST, `simultaneousAppointmentLimit` 1** |
| Per-date detail | Available hours returned per date inside the window |

✅ **The 2026-08-03 expectation was wrong and this is the correction.** The calendar is neither
empty nor default: somebody configured it, and **the self-guided setup corroborates Alex's lock-box
answer precisely**. Self-guided runs evenings and weekends, agent-guided doesn't, which is exactly
the mechanic he described. Tour *parameters* are real, readable, and designable today. Note also
that self-guided hours (06:00-18:00, 7 days) are **wider than the office hours** `getProperties`
reports, so the two sources answer different questions and neither one replaces the other.

⚠️ **What stays uncertain is the booked state, not the configuration.** Alex said Liberty's agents
book **manually**, around their own drive time. A tour that lives in an agent's head or a personal
calendar may never appear here. So read the windows, the lengths, and the limits as truth, and **do
not read an open slot as proof that nobody is standing in that apartment.** That is a narrower and
much more workable caveat than "the calendar may hold nothing," which is what this section said
before the probe.

**Booking is a WRITE:** a lead event with `type: Appointment` / `VirtualTour` / `SelfGuidedTour`
via `sendLeads` or `updateLeads`. Blocked under rule 2.

**Four traps, all real, all ours to solve:**

1. **The write returns no `appointmentEventId`.** Booking is fire-then-verify, not transactional.
   You write, then re-read to confirm it landed.
2. **Nothing in the spec says writing an Appointment consumes a `getCalendarAvailability` slot.**
   So **double-booking prevention is ours to build**, keyed off `simultaneousAppointmentLimit`
   (now a known number: **1** for self-guided). Assume the two systems do not talk to each other
   until proven otherwise.
3. **Time formats differ by method** (see §1.4 table). Normalize once, at the boundary. Everything
   the calendar returns is **MST-labeled**, so the boundary has to carry the zone, not drop it.
4. **The 7-day cap is a design constraint, not a footnote.** Any surface that shows more than a week
   of availability has to make rolling calls and stitch the results, and it has to handle the
   400/308 rather than assume a wide range works.

**GAP.**

1. **Booking is blocked today.** The agent can read availability and propose times; it cannot
   commit one. **This is now the only hard blocker in the section**, and it is rule 2, not Liberty.
2. ✅ **RESOLVED 2026-08-05. `getCalendarAvailability` was probed and it returns real configuration.**
   Granted 2026-08-03 (asked 2026-07-31, asks #3), called read-only 2026-08-05: 30-minute
   appointments, 120-minute lead time, agent-guided M-F 08:00-16:00 MST, self-guided all seven days
   06:00-18:00 MST at a limit of 1. **The planned-for answer of "nothing useful" did not happen.**
   Availability can be surfaced to a prospect today, on reads, without inventing a model from office
   hours. What is *not* resolved is whether the calendar reflects tours Liberty booked by hand, so
   **present it as availability, never as a guarantee.** Also correct the old claim wherever it
   appears: **the window cap is 7 days, not 60.**
3. **Never offer a slot the office can't staff, and note the two sets of hours differ.**
   `getProperties` says M-F 9-5. The calendar says agent-guided 08:00-16:00 and self-guided
   06:00-18:00 seven days. Both are API-readable per property, both are real, and they answer
   different questions. Read them, never hardcode them. Aspen and Summerset will differ.
4. **Double-booking is a design deliverable, not a footnote**, and Alex's answer still makes it
   harder. If the agent books tours against a limit it maintains itself, that state has to live
   somewhere (Supabase) and reconcile against Entrata on every read. The limit itself is no longer a
   guess (**1**, self-guided), **but Liberty's agents book manually and arrange their own drive
   time**, so a manually booked tour may never appear in Entrata for us to reconcile against at all.
   Not decided. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): PROPOSE for agent-guided,
   COMMIT for self-guided.** Liberty's agents book manually around their own drive time, so that
   calendar can be silently wrong and the agent only ever proposes agent-guided times; the
   self-guided limit is real (**1**) and nobody has to drive anywhere, so Foothold owns and commits
   the self-guided booking state. Committing a booking is still a write and waits on the go-live
   signature (§4.4). Closes Tier 3 #9.
5. ✅ **ANSWERED by Alex 2026-08-03: tours are booked MANUALLY.** Asked 2026-07-31 (asks #16),
   answered by email at 08:47 AM PT. **Agents take a call or an email and schedule the tour
   themselves**, because each one manages a portfolio and has to arrange drive time between
   buildings. The other path is **self-guided**: they hand out lock box information and the prospect
   tours alone. There is no shared slot pool and no held slot. **Partly corrected 2026-08-05:** the
   2026-08-05 probe shows self-guided tours *are* configured in Entrata (7 days, 06:00-18:00 MST,
   limit 1), which lines up with the lock-box half of his answer. What remains unknown is whether
   the **agent-booked** half ever lands in the calendar. §1.6 still cannot be designed as a clean
   "read slots, write appointment," but it can be designed as "read real availability, propose,
   confirm out of band." See the REALITY block above, plus gaps 2 and 4.
6. **Self-service tour scheduling is something Brent wants to explore, and it is explicitly NOT
   critical and NOT a blocker (Brent, 2026-08-03).** The shape he described: a Calendly-style booking
   surface, drive-time buffers on both sides of a slot, and an AI agent nudging leads toward adjacent
   slots so an agent's day clusters geographically instead of scattering across a portfolio.
   **Recorded here so it isn't lost and isn't mistaken for a requirement.** It would be
   Foothold-owned scheduling state rather than Entrata's, which is consistent with gap 4 and with
   Liberty's manual mechanic. Later exploration, not scope.

---

## 1.7 Applications

> **INTENT (clarified by Brent, 2026-07-31).** The AI leasing agent **never collects application
> data.** It surfaces Liberty's hosted application URL (ProspectPortal) for the property and hands
> the prospect off: "go apply here." Liberty sets the application up and gives us the URL; Foothold
> just hands it out. Applications are **Entrata's own hosted flow, not ours.** "Applications go
> right through Entrata" always meant the hosted portal, not the API.

**REALITY. This works, and it's clean.** Every capability the API lacks here is exactly what the
hosted flow already handles. **The agent never touches that data, so the API gaps don't touch the
design.**

**There is no hosted application URL anywhere in the spec.** Every `url` / `link` / `redirect`
field in all 302 schemas was enumerated: all of them are media, marketing, or vendor URLs. The
ProspectPortal apply link (`[property].prospectportal.com/.../application_authentication/`) is real
and it works, but **the API will not tell you what it is.** It's a per-property config value you
get out of band from Liberty. That's a config item, not a design problem.

**Sequencing (Brent, 2026-07-31): the apply URLs for NPI's own properties can't exist yet.**
Liberty hasn't onboarded Aspen or Summerset into Entrata, and Aspen doesn't close until 8/31, so
those URLs come after onboarding. Don't treat them as an outstanding ask to Liberty, they're a
downstream dependency on a property that isn't in the PMS.

**But the shape is learnable now.** `getWebsites` returned a dedicated `owyheeparkapartments` site
(id 506) alongside the two Liberty portfolio sites, so Owyhee already has its own web presence and
therefore its own apply link. Pull Owyhee's URL, confirm the pattern, and build the handoff against
a real property. Aspen and Summerset then slot into the same pattern once they're onboarded, with
no design change. This means §1.7 is fully buildable and testable today and does not wait on the
close.

**Handled by the hosted flow, not by us, and therefore not needed from the API:** income,
employment, SSN, identification, documents, consent, answers to the application questions,
screening, and the application-fee payment. Confirming the API can't do any of these is useful only
so nobody scopes work against it later.

⚠️ **One correction to the comfortable version of that sentence, 2026-08-05: not collecting an SSN is
not the same as never seeing one.** SSN / `taxId` appears in the **response** schemas of
`getMitsCollections` r1, `getVendors` r1, and **`getMitsLeases` r1 (`SocialSecurity`), which is on
our key.** The live probe returned none, so Liberty leaves it empty, but **the exposure is on the
read path, not the write path.** Brent's rule, 2026-08-05: **never ingest, persist, or log an SSN**,
enforced by stripping the field at the request choke point on the way in (§2.2). The hosted flow
still keeps NPI out of application PII; this rule keeps us out of the residue.

| API fact | Why it doesn't matter |
|---|---|
| `sendApplication` r1 **creates a shell only.** It takes `propertyId`, `originatingLeadSourceId`, `leasingAgentId`, `unitSpaceId`, `moveInDate`, `leaseTermId`, `floorplanId`, `applicationStatusId`, applicant `name`/`birthDate`/`phone`/`email`/`address`. It accepts **no** income, employment, SSN, identification, documents, consent, or question answers. | Not needed. We don't submit applications. A shell a human then has to finish in the portal is worse than sending the link, and it'd be a write against a live building. |
| **`getCompanyApplications` returns `applicationQuestions[]`** with real examples ("Have you ever been evicted", "Have you ever filed for bankruptcy protection", "Do you smoke"), but `applicationQuestion` **appears in no request schema anywhere.** | Not needed. Readable questions, no answers to submit. The prospect answers them in the portal. |
| **`updateApplication`'s `applicationStatusId` is enum-limited to `[21, 7, 6]`** (completed, approved, cancelled). Terminal states only, no incremental progression. | Not needed. Entrata drives application state; we observe it. |
| **Screening is read-only** (`screeningResultStatus` on `getLeads`, VERIFIED). | Read-only is all we want. Screening is Liberty's and Entrata's. |
| **No payment method exists in the entire spec.** Application fees are *readable* (`getPropertyFees`, `getMitsPropertyUnits`), not *chargeable*. | Not needed. The portal collects the fee. |
| `sendApplication` and `updateApplication` are **UNPROBED**, in the `applications` group, which isn't on the 28-service roster (still absent after the 2026-08-03 grant). | Fine. Don't ask for them. |

**What we still get for free on reads, so the handoff is not a dead end:**

- **`getLeads` returns `screeningResultStatus` and application status**, so Foothold can *observe*
  application progress without touching the flow. The in-progress strings ("Application Started,"
  "Application Partially Completed," "Application Archived") are all VERIFIED in the July pull, so
  abandoned-application re-engagement works today, on reads only.
- **`oa.application.events` is one of the five documented webhook events**, so application stage
  changes become event-driven once webhooks are provisioned. We can follow the prospect through the
  portal without ever being in it.

**DECIDED 2026-07-31. Not a gap. What's left is one config dependency.**

1. **The agent's role is "hand over the link and follow up," full stop.** Not "fill it out," not
   "submit it," not "collect the fee," not "answer the questions." This is what the design already
   said (`lead-comms-spec` §7 "The application itself" = ProspectPortal, self-service, zero
   friction; `system-design.html` data topology: "Applications are ProspectPortal links, not API
   writes"). Brent confirmed API-submitted applications were never the intent.
2. **Config dependency, not a design gap: get the per-property application URL from Liberty.** Not
   discoverable, not syncable, **a manual per-property config item** that has to be collected once
   per property and re-verified when a property is added. **Nobody has asked for Owyhee's URL
   specifically yet** (asks #8). ✉️ **Brent did ask Alex a related question on 2026-08-03, and it is
   still open: does each property have an application link so tenants can apply anytime?** (asks
   #17). That answer shapes the handoff. A standing, always-open link makes this a pure config
   lookup; a conditional one (only when units are available, only during a campaign) means the agent
   has to know whether the link is live before it hands it out. `property-data-capture.html` is the
   right place to store the URL, alongside the §1.5 escalation recipient.
3. **Don't build toward `sendApplication`.** No reason to, and it's a write against a live building.

---

## 1.8 Bulk marketing stays in GHL

> **INTENT.** Email blasts and general marketing communications come from GHL and do **NOT** go into
> Entrata.

**REALITY.** Consistent with Constitution rule 4, which exempts bulk marketing (drips, broadcasts)
from per-contact logging. Also unavoidable: Entrata can't send anything anyway (§1.3), and logging
a broadcast per recipient would be one `updateLeads` write per contact against a rate-limited API on
someone else's org.

One thing to carry: `getMarketingPreferences` returns `optIn` and `consentType` per channel. If a
resident has opted out inside Entrata, GHL will not know unless we read it and sync. **That read is
VERIFIED working as of 2026-08-05** (§1.3), with the customer IDs coming off `getMitsLeases`, so
this stopped being a theoretical sync and became a wiring job. It is worth doing, because sending to
someone who opted out through the PM is a real problem regardless of which system holds the list.
Cache the pick list; at 60 calls an hour it holds the tightest hourly limit on the key (10/min on top
of that), and `getWorkOrderPickLists` holds the tightest daily one at 1,000/day. **Cache both** (§4.2).

**No gap on the rule.** Minor open item: whether Entrata-side opt-outs get synced into GHL.
✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): BOTH.** The **live consent check on the send
path is the load-bearing gate**, and a **nightly sync into GHL runs purely as hygiene** so a GHL
broadcast never hits someone who opted out with Liberty. Cache the pick list, and **a missing
preference means do-not-send.** Closes Tier 3 #10.

---

# Module 2: physical touches (Handwrytten + gift cards)

Fulfillment is settled in `lead-comms-spec` §4: **Handwrytten** for mailed handwritten cards with a
physical gift card enclosed (~$32-34 card + $25 Amazon; ~$4 card-only for sympathy), **Tremendous**
for digital gift cards by text. Neither touches Entrata. **The entire fulfillment half of Module 2
works today with no Entrata permission at all.** What Entrata controls is (a) the trigger data and
(b) the log-back-to-the-PM leg.

💵 **DECIDED 2026-08-13 (Brent, DECISIONS.md session): the annual gift, perk and merch budget is
~$75 per door per year.** The shape: a **Handwrytten welcome packet at move-in**; a **birthday card
with a $25 gift card** (Amazon, or a local Boise merchant for a more thoughtful touch), with the
**Thanksgiving "thankful you're here" card as the no-DOB fallback** (§2.2); and a **$25 Chewy gift
card on the pre-renewal pet card**, or a plain pre-renewal card with $25 for petless households.
Roughly $30-32 all-in per mailed touch ($3-4 Handwrytten card + $25 card + fee). Cheap if the KPIs
hit.

## 2.1 Move-in packet and welcome card

> **INTENT.** Move-in packet and card. Some printed material is provided by Liberty. Foothold mails
> a welcome card telling the resident they can reach out any time.

**REALITY.** Trigger data is **available today**. `getLeases` with **no status filter** returns
exactly the current rent roll (VERIFIED: 52 leases, 49 Current, 3 Notice) with `customers[]`
carrying names, emails, phones, and `moveInDate`, plus `leaseActivities[]` including "Actual Move
In." `getCustomers` is BLOCKED (1403) but is not needed for this.

**GAP.** Only the Entrata **note-back** ("mailed welcome card") is blocked by rule 2 and queues to
the outbox. The card itself mails. Liberty's printed material is an offline coordination item, not
an API one.

## 2.2 Birthday cards

> **INTENT.** Birthday cards. Possibly varied by how many years they've been a resident.

**REALITY. Settled 2026-08-05, and the answer is the opposite of what this section assumed:
birth dates are readable today, but only off the LEAD record, never off the resident record.**

| Source | Birthday available? |
|---|---|
| `getLeads` r1 | ✅ **YES, and it is the source, but it is PARTIAL. VERIFIED 2026-08-05 (third pass): `birthDate` is present on 286 of 787 lead-customers (36%) across the full 2021-01-01 to 2026-08-05 sweep, and it reaches 57 of the 88 current residents (65%).** Requires `propertyId` + `fromDate` + `toDate`; the window cap is **730 days**. ⚠️ **The earlier "122 of 122" reading was wrong**: Entrata omits null fields, so that count only measured records that already carried the key. |
| `getMitsLeases` r1 | ❌ **No.** VERIFIED 2026-08-05: the method works (100 `Tenant` records vs 52 `LA_Lease`) and returns `Finances`, `GuarantorFor`, `Spouse`, and emergency contacts, but ⚠️ **there are no birth dates in the live data.** The spec field is not populated in Liberty's records. |
| `getLeases` r1 + r2 | ❌ **No.** VERIFIED 2026-08-05: **zero birth keys** anywhere in either response. |
| `getCustomers` | **BLOCKED (1403), and it wouldn't help anyway.** Its response schema contains **no birth-date field at all.** Unblocking it does not solve this. |
| `getLeaseDocuments` / `getLeaseDocumentsList` | ⚠️ **NOT on the key**, and as of 2026-08-05 **ruled out on the merits, not just on permission.** It was the last place a DOB could hide, so the Aspen executed-lease set was searched instead (below): **zero birth dates in 44 leases.** Would need a **new** Liberty ask. **Not needed now**, because `getLeads` answers the question, and **not worth asking for later either.** |
| Our own Welcome Profile / pet form | Yes, if the resident fills it out. ⚠️ **REQUIRED, not a fallback (measured 2026-08-05, third pass).** `getLeads` reaches 65% of current residents, so **the Welcome Profile is the only route to the other 35%** and the birthday engine is incomplete without it. |

**The structural fact worth remembering: DOB is captured at the LEAD / APPLICATION stage and never
propagates to the lease or resident record.** That is Entrata's data model, not a Liberty
data-entry gap, and no permission change fixes it.

**Coverage is measured now, and it is 65%. VERIFIED 2026-08-05 (third pass), full historical sweep
2021-01-01 to 2026-08-05.** The rent roll carries **103 customer records across 52 leases, 88 of them
`Current`.** Measured against that roll:

| Measure | Number |
|---|---|
| Current residents on the roll | **88** |
| Appear in `getLeads` at all | **70 of 88** |
| **Carry a `birthDate`** | **57 of 88, so 65%** |
| Never appear in `getLeads` at all | **18** |
| Appear in `getLeads` but carry no birth date | **13** |
| Distinct lead-customers, full sweep | **787** |
| Of those, carrying a `birthDate` | **286, so 36%** |

⚠️ **The "populated, 122 of 122" figure this section carried earlier the same day was wrong, and it
is corrected everywhere in this document.** Entrata **omits null fields** from its responses, so
counting non-empty `birthDate` values against the records that contained the key at all was circular.
It measured nothing. Treat any "N of N" count off an Entrata response with the same suspicion: the
denominator has to come from somewhere other than the field being counted.

**Two consequences, and neither is optional.**

1. ✅ **The join is proven, not assumed.** `getLeads` `customerId` and `getLeases` customer `id` are
   the **same namespace**: **70 of 88** current residents matched on it. The earlier concern that
   lead IDs and resident IDs might be different ID spaces is **closed.** It is the same key the pet
   join (§2.3) and the consent lookup (§1.3) already use.
2. ⚠️ **The Welcome Profile is a required component of the design, not a fallback.** `getLeads`
   covers about **two thirds** of the roll. The remaining third, **31 of 88 residents, has no Entrata
   birthday and never will**, whether because they never got a guest card (18) or because the guest
   card carried none (13). No permission, no sweep, and no other method closes that gap, so the
   recognition engine needs a **first-party capture path** to be complete. ⚠️ **Do not describe
   birthdays as solved.**

**Backfill is cheap, so the historical sweep is not a decision. VERIFIED 2026-08-05 (third pass):
`getLeads` accepts a 730-day window**, and the entire 5.5-year history swept in **3 calls**. The
earliest `moveInDate` on the roll is **2022-06-16**, comfortably inside it. ⚠️ **Window caps are per
method and never transferable.** `getLeads` takes 730 days; `getCalendarAvailability` rejects
anything over 7 with a 400 / 308 (§1.6). **Measure each method. Never inherit a cap.**

**Confirmed from the document side too, 2026-08-05, and this is the counter-example that closes the
question for good.** All **44 executed leases** in Aspen's seller-DD set were text-extracted and
searched (7.4M characters, **no OCR needed**): **zero occurrences of "date of birth" or "birth."**
What each lease *does* carry is one SSN field and one "18 years" adult-occupant clause. So the API
finding and the paper finding agree, from opposite directions: **an executed lease records the
result of screening, never the applicant's DOB.** Practical consequence: **`getLeaseDocuments` is
not a DOB route at any property**, Aspen included, so do not spend a Liberty ask on it and do not
scope a document-parsing pass to chase birthdays.

**Design path.** Pull `birthDate` from `getLeads` and **join to the resident on customer ID**
(`getMitsLeases` returns the same ID as `IDType: "Customer ID"`, which is also what
`getMarketingPreferences` wants, §1.3). ⚠️ **PII rule: store month and day only, drop the year.**
A birthday card needs a date, not an age, and a year of birth is materially more sensitive than a
month and day.

⚠️ **The second standing PII rule, and it is Brent's call, 2026-08-05: never ingest, persist, or log
an SSN.** NPI has no use for it and does not want the liability. It belongs here because the records
that feed recognition are the same records that can carry a Social Security number: SSN / `taxId`
sits in the **response** schema of **`getMitsLeases` r1 (`SocialSecurity`), which is on our key**,
as well as `getMitsCollections` r1 and `getVendors` r1. The 2026-08-05 probe of `getMitsLeases`
returned **no SSN keys**, so Liberty leaves it empty and there is no exposure today, **but the rule
does not wait on the field starting to populate.** Enforcement: **strip `SocialSecurity` / `SSN` /
`taxId` on read, at the single request choke point that already enforces the read-method allowlist,
before anything is persisted, logged, or passed to an AI model.** Fail-safe, at the transport layer,
never a downstream cleanup pass, because a cleanup pass has already lost. Aspen's executed leases
each carry an SSN field, so **the same rule governs lease-document and DD ingestion**, not just the
API. Those two, **month and day only** and **no SSN, ever**, are the standing PII-minimization rules
for everything Foothold reads.

Tenure is computable today from `getLeases` `moveInDate` and lease intervals, so the "varied by
years as a resident" half is fine.

**GAP. Measured 2026-08-05 (third pass). The question is no longer "is there a source" or "how far
back does coverage reach." Both are answered. What is left is who the missing 35% are and how we
reach them.**

1. ✅ **RESOLVED 2026-08-05: there is a live, populated Entrata source for resident birth dates, and
   it is `getLeads`.** This reverses the position held since 2026-07-31 that a resident's birthday
   was unreadable. `getMitsLeases` was granted for exactly this and **does not deliver it**, so ask
   #2 is answered in the sense that matters (the method works) and disappointing in the sense we
   asked for (no DOB). Nothing further is needed from Liberty.
2. ✅ **RESOLVED 2026-08-05 (third pass): the historical sweep is not a design decision, because it
   costs three calls.** `getLeads` takes a **730-day** window, so 2021-01-01 through 2026-08-05 swept
   in **3 calls**. **Run it.** The "start from today forward and let the backfill happen naturally"
   option is strictly worse for free. What the sweep produced is the real answer: **57 of 88 current
   residents carry a birth date, 65%.**
3. ⚠️ **What is left is not a gap in the API, it is a gap in our own data capture, and it is a build
   requirement.** **31 of 88 current residents are unreachable through Entrata for a birthday,
   permanently**: 18 never appear in `getLeads` at all, 13 appear with no birth date. **The Welcome
   Profile is therefore a required component of the recognition engine, not a fallback**, and it
   needs its own capture path, its own nudge cadence, and its own coverage number to watch. What is
   **not decided**: how the Welcome Profile gets in front of a resident (move-in packet insert, a
   link in the welcome text, the resident portal, a small incentive), and what the recognition engine
   does for a resident whose birthday we simply never learn. Those are Foothold design questions, not
   Entrata questions, and no Liberty ask helps. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md
   session): both halves.** (a) **Handwrytten integration is ON** (packet in hand; Brent also
   intends to use Handwrytten personally for business-networking cards), and the Welcome Profile
   reaches the resident as a **mailed Handwrytten welcome packet**. (b) For a resident whose
   birthday we never learn, the annual touch is a **Thanksgiving "thankful you're here" card**,
   keeping the touch count without the date. Two backfill levers shrink the 35% over time: the
   property business plan asks Liberty to **enter DOBs at move-in**, and the **five-line weekly
   digest flags new tenants missing a DOB** (§1.4).
4. **The asymmetry is now explained rather than merely observed.** We can read a *prospect's*
   birthday and not a *resident's* because Entrata only ever stores it on the prospect. Capturing
   `birthDate` at guest-card time and holding it in Supabase is still the right mechanic; the
   difference is that it now works retroactively too, for anyone whose lead record we can still
   reach, **and that the join it depends on is proven rather than assumed** (70 of 88 matched).

## 2.3 Pre-renewal cards, pet and no-pet variants

> **INTENT.** Pre-renewal cards, in a pet and a no-pet variant.

**REALITY.**

- **Renewal timing is readable today.** `getLeases` with no filter gives lease end dates for all 52
  leases, so the ~90-day pre-renewal trigger computes without `getExpiringLeases` (which is BLOCKED
  1403 and not needed). Renewal *state* is also readable: `isRenewal`, `renewalLeaseStartDate`,
  `leaseIntervalTypeName` (Renewal vs New), plus lead statuses like "Renewal Offer Started" and
  "Renewal Lease Approved" (VERIFIED, both present in the July pull).
- **Pet data stopped being the weak spot on 2026-08-05, and `getLeases` r1 is the source.**
  VERIFIED: **30 of 52 leases have pets, 34 individual pet records**, each carrying **`name`**,
  `petType`, `breed`, `color`, `gender`, `age`, `weight`, `moveInDate`, `customerId`, and
  `isAssistanceAnimal`. **Pets join to the resident on `customerId`**, the same key the birthday
  join (§2.2) and the consent lookup (§1.3) already use. That is a real per-resident pet roster with
  the pet's *name* in it, which is exactly what a pet card needs.
- ⚠️ **`isAssistanceAnimal` is load-bearing, and it is a fair-housing rule, not a tact preference.
  Assistance and service animals are NOT pets.** Read the flag on every record and treat it as a
  **hard exclude** from the pet-gift program and from any pet-themed marketing or segmentation.
- **Where not to look.** `getMitsLeases` r1 carries only `Count`, `PetType`, and `Weight`, **no
  name**, so do not use it for pets. `getPetTypes` is **not on the key and is not needed**, because
  `petType` comes back inline. Property-level pet policy is still populated and readable (VERIFIED:
  cats and dogs, $300 deposit, $50/month), and **unit-level `maxNumberOfPets` is still `0` on every
  unit**, which means unpopulated, not "no pets." Ignore that field, it was never the roster.
- **The lead side is fuller still.** `getLeads` r1 returned **126 pet records** (`name`, `petType`,
  `breed`, `color`, `gender`, `age`, `weight`, `count`, `id`, `licenseCity`), so a pet is known at
  guest-card time and carries forward, the same mechanic as `birthDate` (§2.2).
- **One more pet surface with a different job:** `getWorkOrders` r1 carries `petInfo`, useful for
  warning a technician there is an animal in the unit before they walk in (§3.1). That is a safety
  note, not a recognition trigger.
- **Aspen, for comparison:** 21 of the 44 executed leases carry `breed:` / `weight:` labels, so pets
  are documented on paper too, but **no pet-name label**. Entrata is the better source.
- **No renewal-offer method exists.** No `renewalOffer`, no `offerAmount`, no `renewalStatus` in the
  spec. We can *detect* the renewal stage; we cannot create or send an offer. That's Liberty's job
  anyway, and the design already puts the Foothold card **~90 days out, ahead of Liberty's ~60-day
  offer**, so nothing needed changes. Worth stating plainly so nobody scopes it in later.

**GAP. Narrowed hard on 2026-08-05: the pet/no-pet split has a real source, so what's left is
filtering and coverage.**

1. ✅ **RESOLVED 2026-08-05: `getLeases` is the pet roster.** 34 pet records across 30 of 52 leases,
   with names, joined on `customerId`. Our own pet form is now the **fallback**, not the primary
   source, and there is no need to ask Liberty how pet households get recorded on their side.
   **The pet-gift program has a data source**, which means the `pet-gift.html` mockup in the ops
   repo is no longer a mockup waiting on data.
   **DECISION (Brent, 2026-08-05): capture the WHOLE pet record, not just the name.** Store
   `name`, `petType`, `breed`, `color`, `gender`, `age`, `weight` and `moveInDate` on the resident
   profile. The point is the **Handwrytten** card: "we hope Bella is loving the new dog park" reads
   like a landlord who knows the resident, and "your dog" reads like a mail merge. Breed, color and
   age are what let the copy be specific, and age plus `moveInDate` is what lets a card land on a
   pet's move-in anniversary or note a puppy growing up. Capture it on the same recognition-data
   job as the birthday feed, in one pass, since it comes from the same `getLeases` call.
   Pet attributes are not sensitive PII, so they do not fall under the SSN or month/day DOB rules,
   but the assistance-animal exclusion below still governs everything generated from this data.
2. ⚠️ **The assistance-animal exclusion is a build requirement, not a nicety.** `isAssistanceAnimal`
   gets checked on every pet record before anything is mailed, texted, or segmented. Assistance and
   service animals are not pets under fair housing, so a pet-themed touch aimed at one is a
   fair-housing problem, not a charming mistake.
3. **Coverage is better than birthdays, but not perfect.** `getLeases` needs no date window, so the
   current rent roll is covered in a single call with no historical sweep. What it misses is a pet
   acquired mid-lease and never reported to Liberty. The pet form and the Welcome Profile stay as
   the catch-up path for those.

## 2.4 Renewal and referral milestones

> **INTENT.** Renewal and referral system firing at certain milestones.

**REALITY. Referrals cannot be tracked in Entrata at all.**

The string "referral" appears **exactly once in the entire 2.8 MB spec**, as a lead-source name
(`Referral`, Id 790). **No `referredBy` field. No referral method. No referral object.** The
$250/$250 program has to live in GHL, with the code tracked from first touch, and reconciled against
`getLeases` to detect the signed lease that triggers the payout.

Renewal milestone detection is readable (§2.3).

**GAP.** None on intent, this is just a placement fact: **referral is a GHL-owned system, permanently.**
Do not design a referral record into Entrata; there is nowhere to put one. The reconciliation
(referral code in GHL → signed lease in `getLeases`) is the whole mechanism, and it works with reads
only.

## 2.5 Review-gathering cards

> **INTENT.** Possibly review-gathering cards.

**REALITY.** Entrata **does** have a testimonial object, which is more than expected:

| Method | Read/write | Status |
|---|---|---|
| `getCustomerTestimonials` | Read | **NOT on the key.** All three live in the `customers` group, which is absent from the roster in its entirety, so permission is settled without probing |
| `sendCustomerTestimonials` | **WRITE** | **NOT on the key**, and banned by rule 2 regardless |
| `updatePropertyResponse` | **WRITE** | **NOT on the key**, and banned by rule 2 regardless |

Review-ask triggers: move-in +2 weeks and renewal both come from `getLeases` and **work today**.
The third designed trigger, work-order-complete, **also has a data source as of 2026-08-03**:
`getWorkOrders` is permissioned and VERIFIED returning work orders with `status` "Completed" (§3.1).
**All three review-ask triggers are now readable.** One caveat carried from §3.1: filter on
`maintenanceRequestFrom` so an "Entrata PaaS" make-ready on a vacant unit never fires a review ask
at a resident who had no service call.

**Two things the 2026-08-05 probe adds here.** (1) The filtering vocabulary is readable:
`getWorkOrderPickLists` is VERIFIED and returns the property's `StatusTypes`, `Categories`,
`Problems`, and `WorkOrderTypes`, so "which completed ticket earns a review ask" can be expressed
against Liberty's real IDs instead of string-matching English (§3.1). (2) **Consent is a
precondition on the ask itself.** `getMarketingPreferences` works (§1.3), so a review ask sent by
text or email has to check `optIn` for that channel first. That is a gate on the send path, not a
report.

**GAP.**

1. **Nobody has evaluated whether Entrata testimonials are worth using.** They're an Entrata-internal
   object, presumably surfacing on Liberty's own property site. The Foothold design targets **Google
   and Apartments.com** (ILS rank plus the site funnel), which is a different and more valuable
   surface. Options: ignore the Entrata testimonial object entirely, or mirror captured testimonials
   into it so Liberty's site benefits too. Not decided, and low stakes. ✅ **DECIDED 2026-08-13
   (Brent, DECISIONS.md session): SKIP it. Google + Apartments.com only.** An Entrata-internal
   surface on Liberty's website, a new permission ask plus a write authorization, and it moves no
   KPI Foothold is measured on. Closes Tier 3 #11.
2. **Constitution rule 7 is absolute regardless:** trigger on events, ask everyone at that event,
   **no sentiment gating**, never re-ask within ~12 months of a public review. That's FTC / Google /
   Apartments.com, not a style preference.
3. 🆕 **NEW 2026-08-10, and it is genuinely unaddressed: is the work-order-complete review ask the
   same touch as the §3.2 resident follow-up, or a second one?** Brent decided the follow-up is an
   AI-generated personal message about the resident's specific issue. It fires off the **same event**,
   at the **same resident**, through the **same channel** as this ask. Nothing in the corpus says
   whether they are one message or two, or in what order. ⚠️ Two things bound the answer: rule 7 means
   a follow-up must **never** become a sentiment filter in front of the ask, and §6's "don't spam"
   rule means the touches get spaced so it doesn't read like a campaign. **Not decided** (open
   questions Tier 3 #22). ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): TWO messages.**
   Follow-up first, review ask a few days behind, and **the ask fires on the event, never on the
   reply**, which is the only shape that satisfies rule 7 and the don't-spam objection at once.
   Closes Tier 3 #22.

## 2.6 Fulfillment and the log-back leg

> **INTENT.** Uses the Handwrytten API, plus gift cards.

**REALITY.** No Entrata dependency. Both providers are direct API calls. Per `lead-comms-spec` §4:
skip the ABM platforms (Sendoso, Reachdesk carry $15-25K/yr minimums for what amounts to two API
calls).

**GAP.** One, and it's the same one as §1.4: **every touch is supposed to log a note to the Entrata
thread so the PM sees it, and that leg is blocked.** Cards and gifts go out; Liberty sees nothing.
Rule 4's "the PM sees everything" is on hold, not waived.

🔄 **DECIDED 2026-08-05: `sendLeaseActivities` is a NEAR-TERM ASK, so this gap now has a route out.**
(Brent deferred it earlier that day and **reversed himself the same day**.) It is the **only method in
the 128 that appends an activity to a record**, it is a resident / lease-level write, and it would do
exactly this job: put Foothold's touches in Liberty's system **instead of their inbox**. It is **not on
the key**, so it takes a new ask, and that ask **goes in the same email as `sendWorkOrders`** (§3.2,
asks #18, #19). The reasoning is one round trip instead of two: we are already asking for the larger
and more consequential write grant, so the low-blast-radius activity note belongs in the same
conversation, and Liberty hears it as **less mail for them, not more**. ⚠️ **The ask is not the
authorization, and the gap does not close when Liberty says yes.** It is a write, so rule 2 still
requires the per-property lift plus Brent's written sign-off naming the method, and **at Owyhee Park it
stays unused even if granted** (§4.4). The interim answer is unchanged and needs no permission: log the
touch in Foothold's own store, and close Liberty's visibility gap deliberately with a scheduled digest
to the property inbox or a read-only Foothold view they can open. **At Owyhee that interim path is the
permanent one.** 📌 **UPDATED 2026-08-13 (Brent, DECISIONS.md session): the #18/#19 email is TABLED,
not sent, and the interim path is now specified: Foothold's own store plus the five-line weekly
digest email. The read-only Foothold view is OFF the table** (decision #1: live inside Liberty's
ecosystem, no standalone page). While the ask stays tabled, resident-side touches cannot land in
Entrata at all, so the digest is the whole of Liberty's visibility into them.

**And one hard blocker that has nothing to do with the API:** Owyhee Park is Liberty's building, not
NPI's. **There is no NPI-owned property on the key**, so Module 2 has nothing legitimate to run
against until Aspen or Summerset are added. Mailing recognition cards to another owner's residents
is not a permissions question, it's a "don't" question.

---

# Module 3: work-order monitoring and the FlightDeck bridge

## 3.1 Monitoring tickets and work-order completion

> **INTENT.** Monitor tickets and get visibility into work-order completion.

**REALITY. The `maintenance` group landed on the key 2026-08-03, and `getWorkOrders` is VERIFIED
functional against production.**

Liberty granted the group's two reads on 2026-08-03 (asked 2026-07-31, asks #1). **`getWorkOrders`
and `getWorkOrderPickLists` are both on the key. `sendWorkOrders` and `updateWorkOrders` are still
NOT**, and that needs stating precisely: **the group arriving did not carry its writes in with it**,
so their absence is a **deliberate per-method state, not a side effect of the group being off.**
Rule 2 bars them regardless of permission, exactly as it bars `sendLeads` and `updateLeads`, which
*are* permissioned.

**VERIFIED 2026-08-03, real production data.** A read-only `getWorkOrders` r1 call with `propertyId`
`1311115` returned HTTP 200 and real Owyhee Park work orders:

| Field | What came back |
|---|---|
| `maintenanceRequestId` | 3435, 3436 |
| `buildingName` / `unitNumber` | "3030" / "3030-201" |
| `status` | "Completed" |
| `problem` | "Make Ready" |
| `maintenanceRequestFrom` | "Entrata PaaS" |
| Timestamps | MDT |
| Grain | Parent / child, linked by `parentMaintenanceRequestId` |

So the completion trigger is not a schema promise anymore, it's a response we have held in our hands,
and **the turn engine's make-ready signal is present in the data by name.** ✅ **`completedOn` is
OBSERVED**, recorded in the 2026-08-05 pull (`entrata-touchpoints.md` §5.6). `assignedTo` and
`assignedOn` are in the r1 response schema but have **not** been seen in a live response, so they stay
SPEC until observed.

📊 **DECIDED 2026-08-10 (Brent): build the aggregate maintenance-responsiveness metric off this
method. This supersedes the note that used to sit here saying no such metric existed and that
designing one was separate work.** `getWorkOrders` carries **`completedOn` (OBSERVED** in the
2026-08-05 production pull, `entrata-touchpoints.md` §5.6) and **`createdOn` (SPEC**, in the vendor
spec, never observed), and the request supports `createdOnFromDate` / `createdOnToDate` and
`completedOnFromDate` / `completedOnToDate` filters (`entrata-openapi-2026-07-28.yaml`). **`createdOn`
plus `completedOn` makes resolution time computable read-only, with no new permission, no write, no
rule 2 exposure, and nothing to ask Liberty for.**

**It is an aggregate and only an aggregate.** Brent chose aggregate over per-ticket policing, which
**affirms** "watch the scoreboard, not the plays" (`lead-comms-spec.html` §5, escalation step 3) rather
than overturning it, and raises that line from unattributed design intent to a decision he has made.
⚠️ **The "no nightly SLA-policing" line stays and is qualified, not removed: measuring the aggregate is
not policing; alerting on or chasing an individual ticket still is.**

⚠️ **It is dual-purpose, and that is a build constraint, not colour.** Brent's reasoning: *"If that KPI
is genuinely good we can use it in marketing, and if it is bad then it is an accountability metric for
LAG."* Good numbers become marketing proof for the Foothold brand thesis; bad numbers become an
accountability metric in the Liberty relationship. **Both uses demand the same thing: one definition,
written down once, computed the same way every time, and never cherry-picked by window. A number used
in both directions cannot be redefined when it becomes inconvenient.** Write the definition down before
the first chart.

**Build cautions. All of these are established elsewhere in this document; they are restated here
because a naive computation would be wrong:**

| Caution | Why |
|---|---|
| **Skip `maintenanceRequestFrom: "Entrata PaaS"`** (7 of 785) | System make-readies are **turns, not resident issues**, and they would pollute a responsiveness number. |
| **Collapse parent/child on `parentMaintenanceRequestId`** before counting (312 of 785 are children) | Otherwise one turn counts several times. |
| **Do not segment on `maintenancePriority`** | **780 of 785 are `Medium`**, so it carries no signal. Segment on `maintenanceProblem` and `maintenanceLocation`. |
| ⚠️ **`createdOn` is SPEC, not OBSERVED. Verify it comes back populated BEFORE building on it.** | One read-only `getWorkOrders` call settles it. **If it is not populated, the metric needs a different clock and that changes the design.** A field in a schema is not a field in the data: `getMitsLeases`' `BirthDate` is the standing counter-example (§2.2). |
| **Consider reporting in-hours versus out-of-hours separately** | Office hours are **M-F 9:00-5:00 with no weekend coverage**, so roughly **76% of the week has no staff on site** and a raw aggregate hides that. `reference/entrata-api-context.md` §10.4 already makes this point about lead response time; the same logic applies here. |

**Left open on purpose, and this decision closes neither.** (1) **The target or threshold is not set.**
⚠️ The existing **24hr / 4hr** number is a **FIRST CONTACT** target, parked at Phase 2 pending Liberty's
commitment, and it is **not a resolution target.** Do not conflate them. (2) **Whether the metric joins
the six-KPI hero scoreboard on `index.html` or stays a feeder line is not decided.** Open questions
Tier 3 #20. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): both halves. (1) NO target.**
Track the metric for a while and see what it says; **no SLA negotiation with Liberty unless the
data eventually says there should be one.** The 24hr / 4hr first-contact number stays parked at
Phase 2 and stays a first-contact number. **(2) It goes ON the scoreboard: track it and display it,
and that is the whole decision.** Closes Tier 3 #20.

📋 **One compliance note, stated once.** NPI markets under **SEC Rule 506(c)**, so using a real
operating metric in public marketing is permitted. Standard characterization rules apply to any
forward-looking framing.

It also carries **`petInfo`**, worth wiring for a reason that has nothing to do with recognition:
**it warns a technician there is an animal in the unit before they walk in.** Pet data as a
recognition source is §2.3; this one is a safety field on a work order.

**`getWorkOrderPickLists` VERIFIED 2026-08-05, and it carries more than a vocabulary.** A read-only
call returned HTTP 200 with the property-scoped `Locations`, `Priorities`, `StatusTypes`,
`Categories`, `Problems`, and `SubMaintenanceProblems`, plus `WorkOrderTypes` (5) and
`InspectionStatuses` (5). **The one nobody expected: `CompanyEmployees`, 12 rows, Liberty's actual
staff roster.**

| Pick list | What it gives the design |
|---|---|
| `Categories` / `Problems` / `SubMaintenanceProblems` / `Locations` | The real ID vocabulary for classifying a ticket. Filter and report against IDs, not English strings. |
| `Priorities` / `StatusTypes` | What "urgent" and "complete" actually mean in Liberty's config, rather than our assumption of them. |
| `WorkOrderTypes` (5), `InspectionStatuses` (5) | Separates inspections from service calls, which matters for the §3.2 "who gets a follow-up" question. |
| **`CompanyEmployees` (12)** | **Liberty's staff roster, by name.** Work-order **assignment and routing become expressible**: a ticket can be attributed to, or escalated toward, a named person instead of a generic inbox. |

⚠️ **`CompanyEmployees` does not unblock a write.** `sendWorkOrders` and `updateWorkOrders` are
still not on the key and are barred by rule 2 regardless, so **nothing here lets Foothold assign a
work order inside Entrata.** What it enables is entirely read-side and out-of-band: naming the right
human in the §1.5 escalation email, reading `assignedTo` back into a name for FlightDeck, and
partially answering Liberty ask #12 (who covers this property) without another email. Route to a
person, don't dispatch one.

**Three things the real response makes obvious. All are design work, none is a blocker:**

1. **Filter on `maintenanceRequestFrom`.** "Entrata PaaS" means the ticket was **generated by the
   system**, not reported by a resident. A make-ready on a vacant unit is not a service call, and
   firing a "did we take care of you?" text at somebody because an empty apartment got turned would
   be embarrassing. Route Entrata-PaaS-generated make-readies to the turn engine, and
   resident-reported tickets to the review and confirmation engines. **Do not treat the feed as
   homogeneous.**
2. **The grain is parent / child, and it is not an edge case.** `parentMaintenanceRequestId` means one
   resident-visible issue can be several rows. **Measured 2026-08-05: 312 of Owyhee's 785 work orders
   are children.** Collapse to the parent before counting anything or messaging anybody, or one turn
   generates several resident touches and the resident gets three texts for one leak.
3. **Priority is effectively unused, so do not triage on it.** Measured 2026-08-05: **780 of the 785
   tickets are `Medium`** (3 Very High, 1 High, 1 Very Low). `maintenancePriority` carries almost no
   signal at this property. **Triage on `maintenanceProblem` and `maintenanceLocation` instead**, both
   of which resolve against the real `getWorkOrderPickLists` vocabulary above.

**The webhook angle:** `workorder.status.update` is one of exactly five documented Entrata webhook
events, and its payload is `ClientId`, `PropertyId`, `LeaseId`, `CustomerId`,
`MaintenanceRequestId`, `MaintenanceStatus` and **nothing else**. It's an ID and a status. **As of
2026-08-03 that is enough**, because `getWorkOrders` hydrates it into a real record. The webhook is a
live path now rather than a dead end. It still needs Liberty to tick the boxes and Entrata support to
provision the subscription (asks #4), and the receiver must **hydrate, never act on the ID alone.**

**GAP.**

1. ✅ **RESOLVED 2026-08-03. The single highest-value ask landed.** `getWorkOrders` was the top ask in
   `system-design.html` §14 and the thing stranding the turn engine's make-ready signal, the review
   engine's work-order-complete trigger, and all of Module 3's monitoring. **All three now have a
   verified data source.** What replaces the gap is ordinary build work: poll or webhook, collapse to
   the parent, classify by `maintenanceRequestFrom`, and decide what "complete" is supposed to mean
   to a resident.
2. **The read grant opens no write path, and nothing here softens rule 2.** A reachable maintenance
   surface is a reason to be **more** careful, not less: the code-side deny-by-default read-method
   allowlist now sits in front of a group where a mistaken write dispatches a real technician to a
   real occupied unit. `sendWorkOrders` and `updateWorkOrders` are not on the key, and would still
   require Brent's written per-property sign-off even if they were. ⚠️ **CHANGED 2026-08-05, and only
   in one direction: `sendWorkOrders` IS now a live ask, for NPI-owned properties only** (§3.2, asks
   #18). The old instruction here said "must not be asked for" and that half is superseded. **What is
   not superseded:** it is still off the key today, it is still barred by rule 2 today, and it stays
   barred at Owyhee Park permanently because NPI does not own that building. `updateWorkOrders` is
   **not** part of the ask.
3. **Do not build a parallel maintenance record in Supabase as a source of truth.** Cache it,
   annotate it, don't mirror it. Entrata holds the record and now we can read it.
4. **What triggers the service-call follow-up is still NOT decided**, deliberately, and the
   permission landing does not decide it. `system-design.html` §4.2 says so explicitly, and the
   `maintenanceRequestFrom` split above is exactly the distinction that question was waiting on. This
   document does not decide it either. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): POLL,
   not webhook**, with the qualifying statuses resolved against Liberty's real `StatusTypes` and
   **named in config**, and the send goes out **next business morning**, not same day, because a
   text an hour after the tech leaves reads automated. **Plus an AI-judgment gate on which work
   orders deserve a follow-up at all**: not every ticket gets one (no spam), and the gate's rules
   get worked out against real Owyhee work-order examples during build-spec. Closes Tier 3 #21.

## 3.2 Confirming with the resident that the issue got fixed

> **INTENT.** Message or text the resident to confirm their issue actually got taken care of.

**REALITY.** The transport is Twilio (Entrata sends nothing, §1.3), which is fine. **The trigger
exists as of 2026-08-03.** `getWorkOrders` is permissioned and VERIFIED returning `status`
"Completed" on real production work orders (§3.1), and the `workorder.status.update` webhook is
hydratable now that it is. What used to be a missing data source is now a filtering question and a
posture question.

Note the tension with an already-locked decision: `lead-comms-spec` §4 **cut** the automated
post-service satisfaction survey ("reads impersonal and gets unsubscribed"), keeping the physical
service-call card instead. Brent's stated intent here reads like a lighter-touch version of the same
idea: confirm the issue got handled, not survey the experience. Those may be the same thing or two
different things.

**GAP.**

1. ✅ **The trigger exists now, and the first question it raises is which tickets qualify.** §3.1's
   `maintenanceRequestFrom` finding is load-bearing here: an "Entrata PaaS" make-ready on a vacant
   unit must **never** fire a resident confirmation text. Filter to resident-reported tickets,
   collapse to the parent `maintenanceRequestId`, and only then consider messaging. **As of
   2026-08-05 that filter can be written against real IDs**, because `getWorkOrderPickLists` returns
   the property's `StatusTypes`, `Categories`, `Problems`, and a `WorkOrderTypes` / `InspectionStatuses`
   split that separates an inspection from a service call (§3.1). **And any text still has to clear
   the consent check** (`getMarketingPreferences`, §1.3). The no-API paths stay useful regardless:
   the physical service-call card the tech leaves on site, and the ~15-minute post-call text already
   locked for forwarded phone calls. Not decided. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md
   session): an AI-judgment gate decides which work orders deserve a follow-up at all**, on top of
   the mandatory filters: not every ticket gets one (no spam), and the gate's rules get worked out
   against real Owyhee work-order examples during build-spec (§3.1).
2. ✅ **RESOLVED 2026-08-10 (Brent): it is the lighter personal check-in, and it is AI-generated off
   the resident's own issue.** *"Did your back door get fixed?"* **It is NOT the survey**, and the
   `lead-comms-spec` §4 cut **stays cut.** This is the version that survives the objection that killed
   the survey. Full decision, filters, and the new don't-invent-detail rule are in the DECIDED block at
   the end of this section.
3. ✅ **RESOLVED 2026-08-10 (Brent): the per-ticket confirmation does NOT cross the line, and the
   principle itself is AFFIRMED.** Brent chose the aggregate over per-ticket policing, so **"watch the
   scoreboard, not the plays" stands exactly as written** (`lead-comms-spec.html` §5, escalation step
   3) and is now **a decision he has actually made** rather than an unattributed design-intent line.
   **Why the confirmation does not conflict: the message goes to the RESIDENT, not to Liberty.**
   Nothing per-ticket is sent to, demanded of, or logged against Liberty. The posture is "we care about
   you," not "we're auditing Liberty." **Per-ticket outbound to a resident and per-ticket policing of a
   vendor are different acts.** ⚠️ **It is still design intent and still not a Constitution rule**, so
   whether to promote it is unchanged and stays open as Tier 3 #19. ✅ **RESOLVED 2026-08-13
   (Brent, DECISIONS.md session): PROMOTED.** "Watch the scoreboard, not the plays" moves into the
   Constitution. Closes Tier 3 #19.

**DECIDED 2026-08-05 (Brent). The intake question, which is the other half of this section, is
answered, and the answer REVERSES the "Foothold must never be the intake" recommendation this
document carried.**

**TARGET STATE: on NPI-owned properties, once Foothold is live on them, the agent files the work order
itself when a resident reaches out.** Build toward that. It needs two things that are not true yet,
and **both are hard gates, not preferences**:

- **`sendWorkOrders` is NOT on the key**, and the whole `maintenance` group arrived on 2026-08-03 with
  exactly two reads and neither write. **This is a new ask to Liberty that has never been made** (asks
  #18). It is separate from, and larger than, the `sendLeaseActivities` ask that now rides in the same
  email (§2.6, asks #19): same message, two named methods, very different weight.
- **Rule 2 must be lifted for that property**: NPI-owned, Foothold live on it, and Brent signed off in
  writing naming the property and the methods. All three, per the decision block at the top of this
  document and §4.4.

⚠️ **VERIFY THE RESIDENT BEFORE FILING.** Brent's requirement, and it protects the building: **an
unverified ticket sends a real technician to a real unit.** Identifiers, best first:

| Identifier | Strength | Source |
|---|---|---|
| **Phone on file** | **Strongest and cheapest.** An inbound text or call from a number matching the lease is a strong match with **zero friction**. | `getLeases` customer `phone`, `getMitsLeases` `Phone` |
| **Unit number plus one more factor** | Good. The fallback when the number is unrecognised. | `getLeases` / `getMitsLeases` |
| **Date of birth** | ⚠️ **Weak primary, and it must NEVER be the only gate: coverage is 65%** (§2.2), so a third of residents could not pass it at all. Secondary factor only. | `getLeads`, month and day only |

**Fail closed. No match means hand off to a human, never file the ticket anyway.**

⚠️ **UNTIL BOTH GATES CLEAR, and permanently at Owyhee Park: point residents at Entrata's Resident
Portal and do not take the request.** The reason is evidence, not tidiness. Measured 2026-08-05 across
Owyhee's 785 work orders:

| `maintenanceRequestFrom` | count | share |
|---|---|---|
| **Resident Portal** | **390** | **50%** |
| Entrata Core (staff-entered) | 388 | 49% |
| Entrata PaaS (system make-readies) | 7 | 1% |

**Half the building's maintenance already arrives natively through the Resident Portal, and all 71
work-order locations are resident-portal-enabled.** Residents have a working intake path today that
lands directly in the system Liberty's techs work from. Taking the request without being able to file
it means it sits in a queue while the resident believes it has been filed, which is a habitability and
liability problem rather than a UX one, and it creates a split brain against a system already handling
half the building correctly. The follow-up half needs no write and works today: read completion from
`getWorkOrders`, then fire the service-call card and the review ask.

⏰ **The after-hours problem does not go away when the agent can file.** A ticket filed at 11pm Friday
still waits for Monday unless someone is on call, and office hours are **M-F 9:00-5:00 with no weekend
coverage**. **Genuine emergencies get the phone path, never a chat handoff and never an email**, before
and after the rule lifts.

---

**DECIDED 2026-08-10 (Brent). What the resident follow-up actually is. This closes GAP items 2 and 3
above and Tier 3 open question #7.**

> **Brent, 2026-08-10:** *"an AI generated personal message about their specific issue, 'Did your back
> door get fixed?'"*

💬 **It is an AI-generated personal message about the resident's specific issue, and it is NOT the
survey.** `lead-comms-spec.html` §4 cut the automated post-service-issue satisfaction survey because it
"reads impersonal and gets unsubscribed," and **that cut STAYS cut. This decision does not reverse
it.** This is the **lighter personal check-in**, and it is exactly the thing that **survives the
objection that killed the survey**, because it is specific, personal, and comes from the ownership
number rather than a form.

**The posture question is resolved, and here is why it does not conflict with "watch the scoreboard,
not the plays": the message goes to the RESIDENT, not to Liberty.** Nothing per-ticket is sent to,
demanded of, or logged against Liberty. The posture is **"we care about you," not "we're auditing
Liberty."** **Per-ticket outbound to a resident and per-ticket policing of a vendor are different
acts.** The principle itself was affirmed by Brent the same day (§3.1).

**What it requires, all readable today with no new permission:** `maintenanceProblem`,
`maintenanceLocation` and `maintenanceDescription` off `getWorkOrders`. Those three fields are what
make the message specific instead of generic.

**Mandatory filters. Every one of these is decided elsewhere; they are restated here so this touch
inherits them explicitly:**

1. **Skip `maintenanceRequestFrom: "Entrata PaaS"`.** A system-generated make-ready must **never** fire
   a resident message.
2. **Collapse parent/child on `parentMaintenanceRequestId`**, so one turn sends **one** message, not
   several.
3. **Clear the `getMarketingPreferences` consent gate on BOTH the LEAD and the RESIDENT lens** (§1.3).
   **A missing preference means do not send.**
4. **Triage on `maintenanceProblem` and `maintenanceLocation`, never `maintenancePriority`** (780 of
   785 tickets are `Medium`).

⚠️ **A NEW RISK this decision introduces, and it needs a written rule.** The message quotes the
resident's own issue back to them, so **a wrong or stale detail is worse than a generic message.** "Did
your back door get fixed?" lands well when it is right and badly when it is not. The pick-list values
are terse and not always renderable into natural language ("Make Ready" is the obvious example, and it
is exactly the system-generated case filter 1 already excludes). **The rule: the AI renders from the
actual field values and never invents detail. If the problem and location will not form a natural,
confident sentence, fall back to a generic check-in or do not send.** Specificity is the point, but a
confident wrong guess costs more than it earns.

**Constitution rule 8 (fair housing) applies**, as it does to any AI-generated resident-facing message.

📌 **Where the touch gets recorded: not in Entrata.** This is a **RESIDENT** touch, and
`sendLeaseActivities`, the only method in the 128 that appends an activity to a **resident / lease**
record, **is not on the key** (§2.6, asks #19). So the touch logs to **Foothold's own store.**
⚠️ **This strengthens the case for ask #19**: a resident-level service touch is exactly the kind of
thing `sendLeaseActivities` would carry. ⚠️ **CORRECTED 2026-08-10: the stated reason used to open with
"there is no lead-event write method anywhere in the 128-method spec," and that half was wrong.**
`updateLeads` r2 and `sendLeads` r1 both append lead events and both are on the key (§1.4). **The
decision itself is unaffected and the destination is unchanged**, because this message goes to a
resident, not a lead, and the lease-level half of the reasoning stands on its own. **Only the stated
rationale was partly wrong.**

🚫 **Scope, and be unambiguous: this runs on NPI-owned properties once Foothold is live on them. NOT at
Owyhee Park**, which is Liberty's building with Liberty's residents. Everything measured so far was
measured at Owyhee, and **reading their tickets does not licence messaging their residents.**

**Still open, deliberately, and this decision closes none of them:**

- **The completion trigger itself.** Poll `getWorkOrders` versus the `workorder.status.update` webhook,
  and **which `maintenanceStatus` values count as complete enough to ask.** Tier 3 #21. ✅ **DECIDED
  2026-08-13: POLL, with the qualifying statuses named in config off Liberty's real `StatusTypes`.**
- **Timing after completion.** Not set. Tier 3 #21. ✅ **DECIDED 2026-08-13: next business morning.**
- 🆕 **Whether this message and the review ask are ONE touch or TWO.** Nothing in the corpus addresses
  it. Flagged as a **new** open question rather than decided here. Tier 3 #22. ✅ **DECIDED
  2026-08-13: TWO, follow-up first, review ask a few days behind, fired on the event never the reply
  (§2.5).**

## 3.3 Exposing the integration to FlightDeck

> **INTENT.** Expose the Entrata integration, scoped to the Neely Property Investments org only, to
> FlightDeck, so FlightDeck can pull real-time data.

**REALITY. The asset-management half of this is the most buildable thing in the whole document.**

Working today, pure reads, no writes, no agent, no Twilio, no GHL:

| Data | Source | Status |
|---|---|---|
| Full rent roll (52 leases) with scheduled charges | `getLeases`, **no status filter** | VERIFIED |
| In-place vs. market rent, loss-to-lease in $ and % | `getLeases` + `getFloorPlans` + `getUnitTypes` | VERIFIED |
| Renewal exposure by month | `getLeases` lease end dates | VERIFIED |
| NOI actuals, unit-tagged GL line items | `getGlTransactions` (90 accounts, June 2026) | VERIFIED |
| Availability and pricing | `getUnitsAvailabilityAndPricing` | VERIFIED |
| Lead volume, source mix, speed-to-lead clock | `getLeads` (`createdOnDateTime`) | VERIFIED |
| Work orders, incl. make-ready status and completion | `getWorkOrders` (r1, by `propertyId`) | **VERIFIED 2026-08-03** |
| Work-order vocabulary (categories, problems, priorities, statuses, types) **and Liberty's 12-person staff roster**, so tickets resolve to named people | `getWorkOrderPickLists` | **VERIFIED 2026-08-05** |

Two caveats, both already known: **budget is not available** (`getBudgets`, `getBudgetActuals`,
`getBankAccounts` are absent from the accessible list, so NOI-vs-budget sources budget from NPI's
own model), and **the GL feed is a PII feed** (AR Payment memos read like `Firstname Lastname Unit
#XXXX-XXX`). Redact at the transport layer, never log raw bodies. **The SSN scrub belongs in exactly
the same place** (§2.2): `SocialSecurity` / `SSN` / `taxId` get stripped on read at the single
request choke point, so nothing downstream, FlightDeck very much included, ever gets the chance to
store one.

**"Real-time" needs a definition.** Reads are pull-based and rate-limited per method. Availability
is effectively unconstrained (600/min), leases too (1,072/min), but the **serial gate** (one call at
a time, whole account) and the ~8-12K calls/month NPI declared in the access application are the
real limits. Of the five webhook events, three are lease events and one is the application event, so
**webhooks do meaningfully help the FlightDeck refresh story** even though they don't help
availability (there is **no availability event and no pricing event**, so the 12-minute poll stays).
**The fifth, `workorder.status.update`, joined them on 2026-08-03**, because `getWorkOrders` can now
hydrate its ID-only payload (§3.1).

**GAP.** Mostly none on the data. The gap is the word "org," next.

## 3.4 ⚠️ THE NPI ORG PROBLEM

> **INTENT.** Expose the Entrata integration, **scoped to the Neely Property Investments org only**,
> to FlightDeck.

**REALITY. There is no NPI org in Entrata, and there never will be.**

| Assumption | Actual |
|---|---|
| "the NPI org" | The key is scoped to **Liberty Asset Group, org 19530**. NPI has no org and no subdomain. |
| NPI's properties live in NPI's Entrata | Aspen and Summerset will live **inside Liberty's** Entrata org once onboarded, alongside **98 other Liberty properties** (VERIFIED: `getWebsites` enumerates them). |
| Scoping is by org | API access is **org-scoped with per-property grants.** The unit of authorization is a property ID. |

**So "the NPI org" is really "a property-ID subset of a property manager's org."** Three consequences
that need saying plainly:

1. **FlightDeck's multi-tenant story is per-PM-org onboarding, not per-asset-manager.** One asset
   manager with three PMs is three separate Entrata integrations, three API users, three subdomains,
   three key rotations. The tenancy model in FlightDeck has to be built around the PM org, not the
   owner.
2. **The PM is a dependency in every single sale.** Each asset manager's property manager has to add
   an API user, grant the specific properties, and check the right web-service permissions. That's a
   third party who has no contract with NPI, no incentive, and (as Liberty just demonstrated by
   enabling two write services nobody wanted) no particular care about which boxes get ticked.
   **The onboarding friction is the product risk, not the code.**
3. **The property-ID allowlist is a security control, not a safety convenience.** The key can see 98
   Liberty properties. The authorization is one. In a multi-tenant FlightDeck, **the property-ID
   allowlist IS the tenancy boundary**, and a bug in it doesn't leak NPI data to NPI, it leaks
   another owner's building to a customer. Enforce it deny-by-default at the single request choke
   point, next to the read-method allowlist, and fail safe. **That one choke point now carries three
   controls: the read-method allowlist, this property-ID allowlist, and the SSN scrub (§2.2). All
   three fail safe, and none of them is a later hardening pass.**

**GAP.** This is a product-shape question, not an implementation one, and it needs Brent. The
integration can absolutely be scoped to a property-ID list and exposed to FlightDeck. What it cannot
be is scoped to "NPI." Whether the offering is "we integrate your PM's Entrata for you" (services,
high-touch, PM-dependent) or something else entirely is not a decision this document makes.
✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): FlightDeck is INTERNAL-ONLY today, and Brent
is building it toward SELF-SERVE SOFTWARE long-term. It is not a done-for-you services business.**
The three consequences above stay true and are exactly why self-serve does not work yet; they are
the road that has to be engineered, not a reason to sell services in the meantime. Closes Tier 1
#2.

## 3.5 The API-access offering as a product

> **INTENT.** This is the beta test for a full-on API access offering for other asset managers.

**REALITY.** Everything relevant is in §3.4. Add three facts that shape the offering:

1. **Per-client subdomain in the URL path.** `https://apis.entrata.com/ext/orgs/<subdomain>/v1/<group>`.
   The subdomain is the **client's**, so a second client is a second config, not a second parameter.
   Already correctly parameterized in the VPS env shape.
2. **Webhook subscription is not self-serve.** Email `apisupport@entrata.com` with client name,
   subdomain, event types, and receiving URLs, **and** the client has to tick webhook boxes at the
   bottom of the same Web Service Permission tab. Scope is per-client, per-event-type, per-property,
   bound to the same property grant as API access. Every new customer repeats this.
3. **Entrata's onboarding email says "leaving the defaults is recommended," and the default is
   none.** Never tell a PM admin the defaults are fine. Hand them the explicit method list. This
   already cost us once.

**GAP.** The beta is running against **a building NPI does not own**, through a PM relationship that
predates the product idea. Be honest in any status reporting about what's proven (reads against
Owyhee Park) versus what's designed (everything else). 📌 **DECIDED 2026-08-13 (Brent, DECISIONS.md
session): see §3.4. Internal-only today, aimed at self-serve software long-term, not a services
business.**

---

# Cross-cutting

## 4.1 The GHL / Entrata dividing line

> **INTENT.** The dividing line: once they're looking at a particular property, all communication
> goes to Entrata. Before that, it's GHL only.

**This is a good rule. It's crisp, it's testable, and it maps cleanly onto the existing data
topology.** Stated as a rule:

> **Before a prospect names a property, they exist only in GHL. The moment they name a property,
> a guest card is created and every subsequent 1:1 communication mirrors into Entrata. Bulk
> marketing never crosses the line in either direction.**

How it sits against the four stores in `system-design.html` §3:

| Store | Owns | Relative to the line |
|---|---|---|
| **GHL** | Marketing: leads, tags, drips, referral attribution | Everything before the line, plus all bulk marketing forever |
| **Entrata** | Operational record: guest cards, leases, residents, GL | Everything after the line, 1:1 only |
| **Supabase** | Agent state: conversations, resident facts, recognition ledger | Both sides. The agent's own memory. |
| **Cloudflare KV** | Availability cache | Neither. Property data, not person data. |

**⚠️ The critical caveat: today the Entrata side of the line is entirely write-blocked, so
everything past the line queues to an outbox and nothing arrives.** The rule describes the finished
system. In the current system, crossing the line produces a local record and a queued intent, and
Liberty sees exactly nothing. That is a real operational fact, not a technicality: **the PM-visibility
promise is unmet right now**, and either a write sign-off or an explicit interim mechanism has to
close it. ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): both are now named.** The sign-off
is scheduled and scoped (`sendLeads` + `updateLeads` at go-live, §4.4), and the interim mechanism is
the **five-line weekly digest email**, with Entrata's own lead records as Liberty's surface once the
signature lands and **no standalone Liberty-facing page** (§1.4). **One deliberate exception:** escalation to a human doesn't ride the line, because §1.5
sends it out of band (property inbox, Slack) and only files the Entrata note as the record. A
prospect asking for a person never waits on a permission.

Two more things the line does not by itself resolve:

- **What counts as "looking at a particular property"** (see §1.2). ✅ **Decided 2026-08-13: AI
  judgment behind the hard minimum-info gate** (§1.2).
- **Consent and opt-out state live on both sides.** SMS consent is GHL-only (Entrata has no SMS
  consent field to write). Entrata-side opt-outs are readable and unsynced. The line says where
  communications go; it doesn't say who's allowed to be communicated with. ✅ **Decided 2026-08-13:
  Entrata-side opt-outs sync nightly into GHL as hygiene, and the live check on the send path is the
  load-bearing gate** (§1.8).

## 4.2 What's buildable today, reads only

No write permission, no Liberty change, no rule 2 lift required.

| Capability | Method(s) | Module |
|---|---|---|
| Availability + pricing pipeline into KV, `/rentals`, agent `check_availability` | `getUnitsAvailabilityAndPricing`, `getFloorPlans` (sq ft), `getUnitTypes` | 1 |
| Guest-card dedupe (does this email already have a card?) | `getLeads` filtered by email | 1 |
| Lead-event deltas without `getLeadEvents` | `getLeads` with `eventDateFrom` / `eventDateTo` / `eventTypeIds` | 1 |
| **Application handoff: surface the ProspectPortal URL and follow up** (§1.7) | (no Entrata; the URL is per-property config from Liberty) | 1 |
| Application progress tracking (screening result, application status) | `getLeads` `screeningResultStatus` + status | 1 |
| Abandoned-application detection and re-engagement | `getLeads` stale in-progress statuses | 1 |
| **Human escalation, out-of-band leg** (email to the property inbox, Slack, or similar) (§1.5) | (no Entrata) | 1 |
| Speed-to-lead measurement | `getLeads` `createdOnDateTime` | 1 |
| Source-mix and attribution reconciliation (GHL ↔ Entrata by email/phone) | `getLeads` | 1 |
| ID vocabulary pinning for replayable outbox intents | `getLeadPickLists` | 1, 2 |
| Office hours + holiday hours per property | `getProperties` | 1 |
| **Tour availability, real config** (30-min appointments, 120-min lead time, agent-guided M-F 08:00-16:00, self-guided 7 days 06:00-18:00, limit 1). ⚠️ **7-day window cap**, and the booked state is still uncertain because Liberty books by hand (§1.6) | `getCalendarAvailability` r1/r2 (**VERIFIED 2026-08-05**) | 1 |
| **Per-channel consent / opt-out state, as a gate on the send path** (§1.3). Customer IDs come from `getMitsLeases`. **Works on the LEAD side as well as the RESIDENT side, and one customer can hold BOTH sets at once**, so the check has to name which relationship it is testing. Cache the pick list, it is rate-limited to 10/min | `getMarketingPreferences`, `getMarketingPreferencePickList` (**VERIFIED 2026-08-05**) | 1, 2 |
| Move-in, renewal, tenure, lease-end triggers | `getLeases`, no status filter | 2 |
| Renewal-stage detection | `getLeases`, `getLeads` renewal statuses | 2 |
| Property-level pet policy | `getPropertyUnits` | 2 |
| **Per-resident pet roster, with the pet's NAME** (**VERIFIED 2026-08-05**: 34 pet records across 30 of 52 leases; `name`, `petType`, `breed`, `color`, `gender`, `age`, `weight`, `moveInDate`). Join on `customerId`. ⚠️ **Exclude every `isAssistanceAnimal` record, fair housing** (§2.3) | `getLeases` r1 | 2 |
| Prospect pet records at guest-card time (**VERIFIED 2026-08-05**: 126 records, adds `count`, `id`, `licenseCity`) | `getLeads` r1 | 1, 2 |
| Pet-in-the-unit warning for a technician (`petInfo`) | `getWorkOrders` r1 | 3 |
| **Resident birth dates, from the LEAD record, covering 65% of current residents** (**VERIFIED 2026-08-05**: 57 of 88 current residents; 286 of 787 lead-customers carry a `birthDate`). **730-day window**, so the whole history sweeps in **3 calls**. Join to the resident on customer ID, **join proven, 70 of 88 matched**; ⚠️ store month/day only; ⚠️ **the other 35% require the Welcome Profile, which is a required component, not a fallback** (§2.2). **Not** `getMitsLeases`, which returns none | `getLeads` r1 (`propertyId` + `fromDate` + `toDate`) | 2 |
| Resident roster detail: finances, guarantors, spouse, emergency contacts (**VERIFIED 2026-08-05**, 100 `Tenant` vs 52 `LA_Lease`). **No birth dates**, and for pets only `Count` / `PetType` / `Weight`, **no pet name**, so use `getLeases` for pets. ⚠️ Its response schema carries `SocialSecurity`, empty in the live data; **strip it on read regardless** (§2.2) | `getMitsLeases` r1 | 2 |
| **Work-order vocabulary and Liberty's 12-person staff roster** (`CompanyEmployees`), so tickets and escalations resolve to a named human. Read-side only, **no assignment write** (§3.1) | `getWorkOrderPickLists` (**VERIFIED 2026-08-05**) | 3 |
| Rent roll, loss-to-lease, renewal exposure | `getLeases` + `getFloorPlans` + `getUnitTypes` | 3 |
| NOI actuals, unit-tagged GL | `getGlTransactions` | 3 |
| **Work-order monitoring and completion detection** (VERIFIED 2026-08-03) | `getWorkOrders`, `getWorkOrderPickLists` | 3 |
| **Turn / make-ready signal** (filter `maintenanceRequestFrom` = "Entrata PaaS", collapse to parent) | `getWorkOrders` | 3 |
| 📊 **Maintenance responsiveness, as an AGGREGATE resolution-time metric** (**DECIDED 2026-08-10**, §3.1). `completedOn` is OBSERVED; ⚠️ **`createdOn` is SPEC, so verify it comes back populated before building on it.** Request filters `createdOnFromDate` / `createdOnToDate` / `completedOnFromDate` / `completedOnToDate`. ⚠️ Skip `Entrata PaaS` records, collapse parent/child, segment on problem + location and **never** on priority, and consider splitting in-hours from out-of-hours (76% of the week has no staff) | `getWorkOrders` | 3 |
| 💬 **AI resident follow-up after a completed ticket**, rendered from `maintenanceProblem` / `maintenanceLocation` / `maintenanceDescription` (**DECIDED 2026-08-10**, §3.2). Consent-gated on **both** lenses; skip `Entrata PaaS`; collapse parent/child; **never invent detail**. 🚫 **NPI-owned properties only, never Owyhee** | `getWorkOrders` + `getMarketingPreferences` | 3 |
| All Handwrytten / Tremendous fulfillment | (no Entrata) | 2 |

**The asset-management half of FlightDeck is the fastest path to something a partner can look at.**
It's pure read, it depends on no agent, no Twilio, no GHL, and no permission change, and it carries
the least dependency risk of anything in the build.

### ⚠️ Client-build facts that apply to every row above

All three VERIFIED 2026-08-05 (third pass), read-only.

1. 🚨 **Entrata's edge is Cloudflare, and it 403s a default User-Agent. This will bite whoever writes
   the client.** A Python `urllib` call on its **default UA** returned **HTTP 403 with the body
   `error code: 1010`**, which is a **Cloudflare browser-signature ban**, not an Entrata JSON error
   and not a permission problem. The **identical request through `curl` succeeded.** Two
   requirements fall out: **the client MUST set an explicit User-Agent on every request**, and its
   error handling must treat a **non-JSON 403 body as an edge block**, categorically different from a
   400 / 1403 permission denial and from an auth failure. Get this wrong and the first hour of client
   work looks exactly like the API key was revoked.
2. **Window caps are per method and never global.** `getLeads` accepts **730 days**;
   `getCalendarAvailability` rejects anything past **7** with a 400 / 308 (§1.6). Measure the cap on
   each method rather than inheriting one from another.
3. **The plumbing is mapped, and two pieces of it are useful.** `getStatus` returns **HTTP 200 with
   no permissions at all**, so it is a **free liveness check** that spends no business-method budget.
   `getAccessibleClients` returns **200** and re-confirms `libertyassetgroup`, **org id 19530**. The
   three oauth methods (`getAccessToken`, `getJwtAccessToken`, `getUserInfo`) return **403 / error
   1403**, and `getAppAdditionalServices` returns **400 / error 107** because it does not support
   apikey auth. None of that blocks anything, because the gateway takes `X-Api-Key` directly. Also
   worth knowing: **`getLeaseDetails` r2 works with `propertyId` alone**, no lease ID required.

### Measured rate limits, complete as of 2026-08-05 (third pass)

Every method the build depends on now carries measured numbers, which **closes the standing note that
the work-order methods had no measured limit.** (A few daily buckets were never returned in the
headers at all, `getLeases` and `getUnitsAvailabilityAndPricing` among them; those read "not
recorded," not "unlimited.")

| Method | Day | Hour | Minute |
|---|---|---|---|
| `getWorkOrders` | 25,000 | 1,500 | 150 |
| **`getWorkOrderPickLists`** | **1,000, the tightest DAILY bucket on the key** | 500 | 60 |
| `getLeads` | 27,000 | 5,000 | 135 |
| `getLeases` | not recorded | 64,320 | 1,072 |
| `getLeaseDetails` r2 | 50,000 | 5,000 | 1,000 |
| `getMitsLeases` | 20,000 | 4,500 | 200 |
| `getCalendarAvailability` | 48,000 | 2,000 | 120 |
| `getMarketingPreferences` | 10,000 | 1,000 | 60 |
| **`getMarketingPreferencePickList`** | 1,500 | **60, the tightest HOURLY bucket** | 10 |

⚠️ **There are two constrained methods, not one, and they are constrained differently.**
`getWorkOrderPickLists` runs out of **daily** budget first; `getMarketingPreferencePickList` runs out
of **hourly** budget first. Both are vocabularies that barely change. **Cache both,
and never call either one per lead, per resident, or per message.** ⚠️ **Neither holds the tightest
per-minute bucket:** that is `getStatus` at **6/minute**, which is why liveness rides the availability
poll rather than a status ping. Everything else on the key is
generous enough that the **serial gate** (one call at a time, whole account) and the ~8-12K
calls/month NPI declared in the access application remain the binding constraints, not the buckets.

## 4.3 Blocked on Liberty permissions (a checkbox, not a code change)

**Four rows unblocked on 2026-08-03, and all six of those methods are now VERIFIED WORKING as of
2026-08-05.** Liberty's second grant took the key from **22 services to 28**: `getWorkOrders`,
`getWorkOrderPickLists`, `getCalendarAvailability`, `getMitsLeases`, `getMarketingPreferences`, and
`getMarketingPreferencePickList`. **All reads, exactly the six asked for on 2026-07-31, and no new
writes.** The `maintenance` and `communications` groups are no longer absent. Every **read** this
document ever wanted is on the key; the granted rows are kept below rather than deleted so the history
reads straight.

⚠️ **Changed 2026-08-05: two rows on this list are blocked on Liberty again, and both are writes.**
`sendWorkOrders` became a live ask that day (Brent's decision, §3.2), and `sendLeaseActivities` joined
it later the same day when Brent reversed his own deferral (§2.6). **Neither has ever been asked**, and
they go out **in one email, not two**. They are the first two rows below. Two other things are
outstanding with Liberty and are not permission rows at all: **Aspen and Summerset onto the key** (asks
#7, the largest unowned dependency before the 8/31 close) and **the per-property application link**
(asks #17, asked 8/03, unanswered).

📌 **UPDATED 2026-08-13 (Brent, DECISIONS.md session): that email is TABLED, not sent.** #18 and #19
still go out together whenever Brent un-tables it, with the "Foothold" lead source (asks #6) bundled
in, but **nothing is sent now.** And **the Aspen + Summerset item is overtaken by events: Liberty is
already onboarding both properties**, so no email is needed there, just monitoring. Ask #17 remains
the only thing actually in flight.

| Method(s) | Costs us | Asked? |
|---|---|---|
| 🆕 **`sendWorkOrders`** (write, group `maintenance`) | **The whole resident service-ticket path on NPI-owned properties.** Without it the agent can never file a ticket itself and residents keep going to the Resident Portal, which is the correct behaviour today and permanently at Owyhee (§3.2). ⚠️ **Double-gated:** the grant is necessary and not sufficient, because rule 2 still has to lift for that property (§4.4). ⚠️ **Ask it for owned properties only.** `updateWorkOrders` is **not** part of the ask. | ✉️ **NEW AND NOT YET ASKED (Brent's decision, 2026-08-05).** Asks #18. The `maintenance` group landed 2026-08-03 with two reads and neither write, so this has never been requested in any form · ⏸️ **TABLED 2026-08-13 (Brent, DECISIONS.md session): not sent, by decision** |
| 🆕 **`sendLeaseActivities`** (write, group `leases`) | Logging Foothold's own resident touches (birthday card sent, pet card sent, feedback received) into Entrata **instead of Liberty's inbox**. The only method in the 128 that appends an activity to a record. ⚠️ **Resident / lease level only:** the lead-level equivalent does not exist anywhere in the spec and never will (§1.4). ⚠️ **Double-gated like #18:** a grant does not lift rule 2, and **Owyhee stays unused even if granted** (§4.4). | ✉️ **NEW AND NOT YET ASKED (Brent, 2026-08-05, reversing his own deferral the same day).** Asks #19, sent **in the same email as #18** (§2.6) · ⏸️ **TABLED 2026-08-13 (Brent, DECISIONS.md session): not sent, by decision.** While tabled, resident touches log to Foothold's store and surface in the weekly digest (§1.4) |
| **`getWorkOrders` + `getWorkOrderPickLists`** | **Nothing anymore.** Unblocked the whole of Module 3's monitoring, the turn engine's make-ready signal, the review engine's work-order trigger, and the `workorder.status.update` webhook (which the hydration makes usable). **Both VERIFIED functional**, §3.1, and the pick lists carry Liberty's 12-person staff roster. | ✅ **GRANTED 2026-08-03 · VERIFIED 2026-08-05** |
| `getMitsLeases` | **Nothing on permission, and it works.** 100 `Tenant` records, finances, guarantors, spouse, emergency contacts, and the customer IDs `getMarketingPreferences` needs. ⚠️ **But no birth dates in the live data**, so it does not serve the birthday card; `getLeads` does (§2.2). | ✅ **GRANTED 2026-08-03 · VERIFIED 2026-08-05** |
| `getCalendarAvailability` | **Nothing, and it returns real configuration**, not the empty shell we expected: 30-min appointments, 120-min lead time, agent-guided M-F 08:00-16:00 MST, self-guided 7 days 06:00-18:00 MST at limit 1. ⚠️ 7-day window cap, not 60 (§1.6). | ✅ **GRANTED 2026-08-03 · VERIFIED 2026-08-05** |
| `getMarketingPreferences` + `getMarketingPreferencePickList` | **Nothing, and both work.** `optIn` per consent type per channel, IDs sourced from `getMitsLeases`. This is the consent gate in front of any 1:1 message (§1.3), and it **works on the LEAD side as well as the RESIDENT side, with one customer able to hold both sets** (VERIFIED third pass). ⚠️ Pick list carries the tightest **hourly** limit on the key (1,500/day, 60/hour, 10/min); `getWorkOrderPickLists` carries the tightest **daily** one (1,000/day). **Cache both.** | ✅ **GRANTED 2026-08-03 · VERIFIED 2026-08-05** |
| `getLeaseDocuments` / `getLeaseDocumentsList` | **Not on the key.** The only route to an executed lease document. Confirmed absent 2026-08-05 while chasing birth dates; ⚠️ **would take a new Liberty ask.** **Not needed**, because `getLeads` carries the DOB (§2.2), and now **ruled out on content, not just permission:** all 44 of Aspen's executed leases were text-extracted and contain **zero birth dates** (they do each contain an SSN field, so this route is a heavier PII surface for no gain). Don't ask for it. | ❌ **Not asked.** No current need, and no DOB in there anyway |
| `getCustomerTestimonials` (and the testimonial writes) | Entrata testimonial object. Low value: Foothold's review targets are Google and Apartments.com. | ❌ **Not asked, on purpose.** Not urgent |
| `getCustomers` | Standalone resident lookup. **Even unblocked it has no birth-date field**, so it does not solve birthdays, and resident data already arrives via `getLeases` and `getLeads`. | ❌ **Dropped from consideration entirely.** Not deferred |
| `getPropertyMedia` | Photo library, so photo completeness on the ILS scorecard. | ❌ **Not asked, on purpose.** Convenience, not urgent |
| `getExpiringLeases` | Nothing. `getLeases` carries lease end dates. **Stop specifying it in flows.** | ❌ **Not asked, on purpose.** Redundant |
| `getLeadEvents` | Fewer calls and cleaner deltas. Nothing else: `getLeads` r1's `eventDateFrom` / `eventDateTo` / `eventTypeIds` filters cover the need. | ❌ **Not asked, on purpose.** Covered by `getLeads` |
| `getBudgets` / `getBudgetActuals` / `getBankAccounts` | NOI-vs-budget from Entrata. Absent from the accessible list entirely. Budget comes from NPI's model until then. | Not in the 2026-07-31 email. Still on the list (#13) |
| Webhook event subscriptions | Real-time lease and application events. Needs Liberty ticking boxes **and** an Entrata support email. | Not in the 2026-07-31 email. Still on the list (#4) |

## 4.4 Blocked on Brent's write sign-off (Constitution rule 2)

Everything here has the permission on the key and is barred by our own rule. Lifting it takes
Brent's explicit written per-property sign-off naming the property and the methods.

📌 **DECIDED 2026-08-05: the lift is scheduled, and its shape is now specified. Nothing below is
authorised today.** Brent's decision: rule 2 **will** be lifted on **NPI-owned** properties when
Foothold goes live on them (Aspen, then Summerset). **Three conditions must ALL hold before a single
write is issued: (1) NPI owns the property, (2) Foothold is live on it, (3) Brent has signed off in
writing naming that property and those methods.** Until all three hold the answer is no, and a
decision to lift later is not a lift now.

⚠️ **Owyhee Park is NOT covered and is not expected to be.** It is Liberty's building, not NPI's, and
a property NPI does not own stays read-only regardless of what the key is permissioned for. Everything
in this document was measured there, so **a working probe is not a licence to write.**

⚠️ **Build consequence, and it is the one that matters: the deny-by-default allowlist does not go
away, it becomes PER-PROPERTY.** Same single choke point, same fail-safe. What changes is that a named
property can carry a named set of permitted write methods, rather than the list being empty everywhere
forever. **A missing, unparseable or unrecognised property configuration still means read-only. Build
it per-property from the first line of client code**, because retrofitting a per-property gate onto a
global bar is exactly where the accident happens. The property-ID allowlist (§3.3) sits at the same
choke point and is unchanged.

📝 **DECIDED 2026-08-13 (Brent, DECISIONS.md session): what the first sign-off NAMES.** The go-live
signature for a property names **`sendLeads` + `updateLeads`**. **Work orders and lease activities
come on a second signature later**, once granted: filing a work order sends a real person into an
occupied unit, a different blast radius from creating a guest card. One scope consequence worth
stating: **lead-event / transcript logging rides on `updateLeads`, so logging all lead interactions
sits inside that same first signature**; nothing separate is needed for it. Nothing is signed
today, and the three conditions above still all have to hold first.

**Writes on the key are UNCHANGED at exactly two, confirmed on the 2026-08-03 probe: `sendLeads` and
`updateLeads`.** Liberty's second grant added six reads and **zero** writes.

| Write | Blocks |
|---|---|
| `sendLeads` | **Guest-card creation**, so the entire hinge of Module 1's dividing line |
| `updateLeads` | **All communication logging**, so rule 4's PM-visibility promise, plus recognition-touch notes, review-ask notes, the escalation note leg (§1.5, the record only, not the leg that reaches a human), tour-unconfirmed flags |
| Lead event `type: Appointment` (via either method) | **Tour booking** |
| `sendWorkOrders` | **Resident service-ticket filing**, plus ticket routing and emergency escalation. **Still not on the key**, even though the `maintenance` group is and both of its reads were granted, so the withholding is **per-method and looks deliberate**. **Double-blocked, and both blocks are live: it is not permissioned, and rule 2 bars it.** ⚠️ **Changed 2026-08-05: it IS now a live ask, for NPI-owned properties only** (§3.2, asks #18), which reverses the old "do not ask for it" note. A grant would clear only the first block. `updateWorkOrders` is **not** part of the ask and stays out of scope. |
| `sendLeaseActivities` | Logging Foothold's own resident touches into Entrata (§2.6). **Not on the key.** ⚠️ **Changed 2026-08-05: it IS now a live ask** (asks #19, in the same email as #18), which reverses the deferral recorded earlier the same day. **A grant clears only the permission block.** It would still need the per-property sign-off, **it stays unused at Owyhee even if granted**, and **its lead-level equivalent does not exist at all** (§1.4). |
| `sendApplication` / `updateApplication` | Nothing we need (§1.7: applications are the hosted portal by design). UNPROBED and in a group that isn't on the roster. Don't ask for them. |

**And they keep the permission, permanently.** Brent **declined on 2026-07-31** to ask Liberty to
uncheck `sendLeads` and `updateLeads` (asks #5), so write capability stays live on the key by
choice. **The email that would have carried that ask was drafted 2026-07-30, never sent, and
deleted on 2026-07-31. There is no pending email and nobody should recreate one.** **Assume write
capability is always there.** Nothing on the vendor side will ever stop a write; the
code-side deny-by-default read-method allowlist at the single request choke point is the only
control, and rule 2 is the only authorization gate. Both get more load-bearing because of this
decision, not less.

**Note the ordering problem in Module 1.** The guest card is the trigger for the logging half of
everything downstream (agent conversation logging, tour booking, the escalation note), and it's a
write. So **the mirror-into-Entrata half of Module 1 is gated behind one signature.** What is *not*
gated: Module 2's fulfillment, Module 3's reads, the application handoff (§1.7), and the leg of
escalation that actually reaches a human (§1.5). Those run without the signature.

## 4.5 Not possible in Entrata at all

No permission change and no sign-off fixes any of these. They live in GHL, Supabase, or a
Foothold-owned store, permanently.

| Capability | Why | Where it has to live |
|---|---|---|
| **Sending any SMS, email, chat, or push** | No send method in 128 methods | Twilio / GHL / the site widget |
| **Referral tracking** | "referral" appears once in 2.8 MB, as a lead-source name. No field, no method, no object. | GHL, reconciled against `getLeases` |
| **A task, ticket, reminder, or work queue for a prospect** | No such object anywhere. Zero keys matching task/follow/reminder/escalat/notify. | Out-of-band notification (property inbox email, Slack, or similar) plus an Entrata note for the record. Settled, §1.5. |
| **Setting lead status** | No status param on any write | Ours, or accept Entrata's assignment |
| **Submitting application answers, income, employment, SSN, ID, documents, consent** | No request schema accepts them | ProspectPortal, hosted by Entrata. **By design, §1.7.** We never collect this data. |
| **Charging an application fee** | No payment method in the entire spec | ProspectPortal. By design, §1.7. |
| **Creating or sending a renewal offer** | No renewal-offer method | Liberty, natively |
| **Writing SMS consent** | No `updateMarketingPreferences`, no SMS field on `sendLeads` | GHL |
| **Amenity content and concessions** | `getAmenities` and `getSpecials` both return **zero** | `foothold-web`, curated, with a named owner and a staleness policy |
| **The ProspectPortal apply URL** | No hosted-application URL field anywhere in the spec | Per-property config, collected from Liberty. A config dependency, not a design gap (§1.7). |
| **Availability or pricing push events** | Not among the 5 webhook events | The 12-minute poll stays |
| **Attribution as a Foothold lead source** | No Foothold source in Liberty's pick list | GHL, until Liberty adds one |

## 4.6 Webhooks: what they do and don't change

Documented, real, and vendored verbatim at `reference/entrata-webhooks-2026-03-04.md`.

**Exactly five events:** `lease.completed`, `lease.partially.completed`, `lease.started`,
`oa.application.events`, `workorder.status.update`. **No availability event. No pricing event. No
documented lead-created event.**

| Property | Detail |
|---|---|
| Payload shape | Amazon SNS shaped: `Type` / `MessageId` / `TopicArn` / `Message` / `Timestamp` / `SignatureVersion` / `Signature`. **`Message` is a JSON string requiring a second parse.** |
| Content | **IDs and a status only, never the record.** A `workorder.status.update` is `MaintenanceRequestId` + `MaintenanceStatus`. **Resolvable as of 2026-08-03**, because `getWorkOrders` is permissioned and verified and hydrates the ID into a real record (§3.1). **Hydrate, never act on the ID alone.** |
| Auth | **RSA public-key signature.** SignatureVersion 1 = SHA1withRSA, 2 = SHA256withRSA. ⚠️ The doc's prose and its own sample code **disagree** on whether `Subject` or `TopicArn` is in the signed string. Handle both. |
| Delivery | **At-least-once**, stable `MessageId` across retries. **Dedupe on it.** Retry count, backoff, ordering, and dead-letter are all **undocumented**. |
| Subscription | Not self-serve. Email `apisupport@entrata.com`, **and** the client ticks webhook boxes on the Web Service Permission tab. Per-client, per-event-type, per-property, bound to the same property grant. |
| Receiver | Public HTTPS endpoint returning 2xx. **Cloudflare Worker.** Does not disturb the VPS outbound-only rule (Constitution rule 1). |
| Unknown | Plan for an SNS-style `SubscriptionConfirmation` handshake even though the doc never mentions one. |

**What they change:** lease-lifecycle and application events become push, which helps Module 2's
triggers and Module 3's FlightDeck freshness. **And as of 2026-08-03 the work-order event is a real
path too**, because `getWorkOrders` hydrates its ID-only payload (§3.1). **What they don't change:**
the availability poll stays (no availability event, no pricing event), and none of this exists until
Liberty ticks the boxes and Entrata support provisions the subscription (asks #4).

✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): PROVISION NOW, bundled into the
Aspen/Summerset onboarding Liberty already has in motion** (asks #4 rides inside that setup, not as
its own round trip). Liberty ticks the webhook boxes on the same Web Service Permission screen used
for API grants (Manage → Web Service Permission tab → bottom of page → select webhooks → Save). We
stand up a receiver URL and **observe what arrives during normal Owyhee activity: read-only, zero
risk.** Observation answers the bundled question (does anything fire on lead or guest-card
creation, e.g. `lease.started` at guest-card stage), and the signature discrepancy goes to Entrata
support in the same round (Entrata asks #2). **The 12-minute availability poll stays the design
either way** (five published events, none is availability). ⚠️ **One conflict to reconcile at
build-spec, noted 2026-08-13: the answer as recorded says the receiver URL stands up on the VPS,
while this section has the receiver as a Cloudflare Worker precisely so the VPS stays outbound-only
(Constitution rule 1).** The decision decides *that* we provision and observe; **where the receiver
lives gets settled expressly in the build spec, and rule 1 is not amended by this filing.** Closes
Tier 3 #13 in practice: lease events go push once provisioned, availability keeps polling, and the
work-order follow-up trigger polls by decision (§3.1).

---

# Asks for Liberty Asset Group

Ranked by what they unblock. **Numbering is stable so cross-references elsewhere in this doc keep
working.**

> ### 📌 Status, 2026-08-13 (DECISIONS.md session): the near-term list is CUT to one item in flight
>
> **#7 (Aspen + Summerset onto the key) is OVERTAKEN BY EVENTS: Liberty is already working on
> onboarding both properties.** No email needed; monitor progress. Aspen still does not inherit
> writes on arrival (§4.4), and the first signature names `sendLeads` + `updateLeads` only.
> **#18 (`sendWorkOrders`) and #19 (`sendLeaseActivities`) are TABLED, not sent**, and **#6 (the
> "Foothold" lead source) bundles into that same email whenever it goes.** While tabled,
> resident-side touches cannot land in Entrata, so they log to Foothold's own store and surface in
> the five-line weekly digest (§1.4). **#4 (webhooks) goes the other way: provision now, bundled
> into the Aspen/Summerset setup already in motion** (§4.6). **#17 (the per-property application
> link, asked 8/03) remains the only thing actually in flight with Liberty.**
>
> ### ✅ Status, 2026-08-05: items 1, 2, 3, 15, and 16 are ANSWERED, and every granted method is now PROBED
>
> **All six granted methods were called read-only on 2026-08-05 and all six returned HTTP 200 with
> real data.** Nothing is waiting on a probe. Two results changed the design rather than confirming
> it: **`getMitsLeases` carries no birth dates** (so ask #2 was granted but does not deliver what it
> was asked for; `getLeads` does, §2.2), and **`getCalendarAvailability` returns real tour
> configuration** rather than the empty calendar Alex's manual-booking answer led us to expect
> (§1.6). **Nothing new needed asking of Liberty as a result of the probes**, and at that point item
> 17 was the only thing in flight.
>
> ### 📌 Superseded the same day by Brent's decisions. There are now FOUR near-term asks.
>
> **In priority order: (#7) Aspen and Summerset onto the key**, the largest unowned dependency before
> the 8/31 close and the thing Module 2 has been waiting on since 2026-07-31; **(#17) the per-property
> application link**, asked 2026-08-03 and still unanswered; **(#18) `sendWorkOrders` for
> NPI-owned properties**, new on 2026-08-05 and **never asked in any form**; and **(#19)
> `sendLeaseActivities`**, added later the same day when Brent **reversed** his own deferral of it.
> 🔄 **#18 and #19 go out in ONE email, not two.** #18 is the larger and more consequential write
> grant, so the low-blast-radius activity note belongs in the same conversation rather than a second
> round trip. ⚠️ **Neither grant authorises a call:** rule 2 gates both, per property, in writing.
>
> **Superseded, kept for history. Status, 2026-08-03: items 1, 2, 3, 15, and 16 are ANSWERED.**
>
> **Alex Costa replied.** The three permission asks (#1, #2, #3) were **granted in full**: all six
> methods, all reads, key now at **28 services, up from 22**, and **no new writes**. The two
> operating questions (#15 escalation, #16 tour booking) were **answered by email at 08:47 AM PT on
> 2026-08-03**. **Nothing from the 2026-07-31 email is outstanding. Do not re-send any of it.**
>
> Item **5 is closed, declined by Brent on 2026-07-31**, and stays on the list as a record so nobody
> re-raises it. The email that would have carried it was drafted 2026-07-30, **never sent, and
> deleted on 2026-07-31**. There is **no pending email** for it anywhere in this file.
>
> Items **9, 10, 11, 12, 13, and 14** were **deliberately left out** of the 2026-07-31 email to keep
> the ask short and answerable, and **are still not asked.** Read the **"Deliberately NOT asked"**
> block below before widening the list.
>
> ✉️ **NEW AND OPEN: item 17.** Brent asked Alex on **2026-08-03** whether **each property has an
> application link so tenants can apply anytime.** **Unanswered as of this writing.** It is the only
> thing in flight with Liberty right now.
>
> **Superseded, kept for history. Status, 2026-07-31:** items 1, 2, 3, 15, and 16 were sent to Alex
> Costa (alex@libertyassetgroup.com, cc Taylor Grange) on 2026-07-31 in the "Entrata API user for
> Liberty" thread and were in flight awaiting his reply. All five are now answered, above.

| # | Ask | Unblocks |
|---|---|---|
| 1 | ✅ **GRANTED 2026-08-03. `getWorkOrders` and `getWorkOrderPickLists`** (read, group `maintenance`) are on the key. Asked 2026-07-31. **`getWorkOrders` is VERIFIED functional** against Owyhee production (§3.1), and **`getWorkOrderPickLists` was VERIFIED 2026-08-05**. It returns the full ticket vocabulary **plus `CompanyEmployees`, Liberty's 12-person staff roster**, so routing to a named human is a read, not a write. | Unblocked all of Module 3's monitoring, the turn engine's make-ready signal, the review engine's third trigger, and the work-order webhook. **Was the highest-value ask by a wide margin, and it landed.** The group's writes (`sendWorkOrders`, `updateWorkOrders`) were **not** granted, must not be requested, and would be barred by rule 2 anyway. |
| 2 | ✅ **GRANTED 2026-08-03, PROBED 2026-08-05. `getMitsLeases`** (read, group `leases`) works: 100 `Tenant` records vs 52 `LA_Lease`, plus finances, guarantors, spouse, and emergency contacts. ⚠️ **It carries NO birth dates.** | **The ask was granted and did not deliver what it was asked for.** Resident DOB is not on the lease record and never will be; it lives on the **lead** record, where `getLeads` returns it for **65% of current residents, 57 of 88** (§2.2; the "122/122" figure recorded earlier the same day was an artifact of Entrata omitting null fields and is corrected). What `getMitsLeases` *does* deliver is the customer-ID source that makes `getMarketingPreferences` usable (§1.3). `getCustomers` still does not solve DOB (no birth-date field) and is still absent from the key. **No follow-up ask needed.** |
| 3 | ✅ **GRANTED 2026-08-03, PROBED 2026-08-05. `getCalendarAvailability`** (read, group `properties`) **and `getMarketingPreferences` + `getMarketingPreferencePickList`** (read, group `communications`) all work. | **Both answers were better than expected.** The calendar holds **real configuration** (30-min appointments, 120-min lead time, agent-guided M-F 08:00-16:00 MST, self-guided all 7 days 06:00-18:00 MST at limit 1) rather than the empty shell #16's manual-booking answer implied; ⚠️ the live window cap is **7 days, not 60** (§1.6). Consent returns `optIn` per consent type per channel and is now a **gate on the send path** (§1.3). ⚠️ The pick list carries the tightest **hourly** limit on the key (1,500/day, 60/hour, 10/min), and `getWorkOrderPickLists` the tightest **daily** one (1,000/day). **Cache both.** |
| 4 | **Enable webhooks** for `lease.completed`, `lease.started`, `oa.application.events`, and `workorder.status.update` (bottom of the Web Service Permission tab). Requires an Entrata support email in parallel (see below). ✅ **DECIDED 2026-08-13 (Brent, DECISIONS.md session): provision now, bundled into the Aspen/Summerset onboarding already in motion; observe what arrives at Owyhee read-only** (§4.6). | Push events for Modules 2 and 3. The 12-minute availability poll stays regardless. |
| 5 | ❌ **CLOSED. DECLINED BY BRENT 2026-07-31. Do not ask, do not re-raise, do not re-draft the email.** The ask would have been "uncheck `sendLeads` and `updateLeads` on the API user" (two checkboxes, not four: `sendWorkOrders` and `updateWorkOrders` were never granted, so there was nothing to turn off there). **The email carrying it was drafted 2026-07-30, never sent, and deleted on 2026-07-31.** It no longer exists and **there is no pending action here.** **Brent is accepting the risk and leaving both write permissions enabled.** His reasoning: the permission being on is not what causes a write, our code calling a write method is, and it isn't worth a second round-trip with the PM. | Nothing, by choice. **The consequence cuts the other way, so read it carefully: there will be no vendor-side guard, permanently and deliberately. The code-side deny-by-default read-method allowlist is the sole control and always will be, which makes it more load-bearing, not less.** Constitution rule 2 is unchanged and undiluted: no writes against a live production property, ever, without Brent's written per-property sign-off. |
| 6 | **Add a "Foothold" lead source** to the property's pick list. 📌 **2026-08-13 (Brent, DECISIONS.md session): bundle into the #18/#19 email whenever it goes; nothing is sent now** (that email is tabled). **Build the GHL-to-`getLeads` reconciler regardless**: it is needed until the source exists and stays the audit path afterward (§1.2). | Attribution, and the demand-side thesis being measurable at all. Nobody has asked yet. |
| 7 | 🔺 **THE LARGEST UNOWNED DEPENDENCY BEFORE THE 8/31 CLOSE. Add Aspen and Summerset to the key** (and North River when it moves onto Entrata). What does it require, what's the lead time? ✅ **OVERTAKEN BY EVENTS 2026-08-13: Liberty is already working on onboarding both properties.** No email needed; monitor progress. Writes still do not arrive with the property (§4.4). | Every engine that needs an NPI-owned building. Module 2 has nothing legitimate to run against until this lands. ⚠️ **Sharper as of 2026-08-05:** the rule 2 lift applies to **NPI-owned properties only** (§4.4), so **Aspen landing on the key is the gate on the entire write half of the design**, not just on Module 2's fulfillment. Owyhee can never carry it. |
| 8 | **Provide the hosted application URL (ProspectPortal) for Owyhee Park now.** Aspen and Summerset come later, see below. | **The leasing handoff (§1.7).** The agent hands this URL to the prospect; it's the whole application design. Not discoverable through the API, so it's a manual config item. **Ask for Owyhee's only.** Per Brent 2026-07-31, Aspen and Summerset aren't onboarded to Liberty's Entrata yet (Aspen closes 8/31), so their URLs can't exist until then. Getting Owyhee's confirms the URL pattern and unblocks building the handoff today; the NPI properties slot into the same pattern later with no design change. Don't ask Liberty for URLs to properties they don't have yet. |
| 9 | **Confirm office hours** (M-F 9-5, no weekend coverage) are current and not seasonal. | The whole after-hours speed-to-lead argument rests on this number. |
| 10 | **Confirm the make-ready SLA.** The flows assume 3-4 days; the probe showed 1 day on two units. | Any vacancy-day math. |
| 11 | **Amenities and specials return zero.** Data-entry gap, or genuinely nothing configured? If you run concessions, where do they live? | Agent pricing accuracy. If a special isn't in Entrata, the agent quotes list and is wrong. |
| 12 | **Which of the 15 leasing agents cover this property**, and do you want routing to a named agent or a pool? ⚠️ **Partially answered by data, not by Alex:** `getWorkOrderPickLists` returns `CompanyEmployees`, 12 people, property-scoped (§3.1, VERIFIED 2026-08-05). That is a roster, not a coverage policy. | Escalation routing. The names are readable now; **the preference (named agent vs. pool) still isn't**, and it's the half that needs a human answer. Ask only if escalation routing gets built before Aspen. |
| 13 | **Is budget data available at all** (`getBudgets` / `getBudgetActuals`)? | FlightDeck's NOI-vs-budget tile, or it falls back to NPI's model. |
| 14 | ❌ **Deliberately left off the 2026-07-31 email. Enable `getPropertyMedia`** (read). | Photo completeness on the weekly ILS scorecard. Convenience, not urgent, so it was cut to keep the ask short. |
| 15 | ✅ **ANSWERED 2026-08-03. Email Liberty's direct leasing / manager team;** `adrienne@libertyassetgroup.com` today. They run Google / Breakroom chats but **prefer email.** ⚠️ **The team is being restructured, so treat the recipient as per-property config, not a constant.** | §1.5's out-of-band leg, now built on a fact instead of an assumption. Store the address with the other per-property config (`property-data-capture.html`), re-verify when a property is added or a reorg lands, and never hardcode it into the escalation path. |
| 16 | ✅ **ANSWERED 2026-08-03. Tours are booked MANUALLY.** Agents take a call or an email and schedule it themselves, because each manages a portfolio and has to arrange drive time. The alternative is **self-guided**: they hand out lock box information. | §1.6 tour scheduling, and it changes the premise. There is no shared slot pool, no held slot, and no indication tours land in Entrata's calendar, which is why `getCalendarAvailability` being permissioned settles less than it looks (§1.6, gap 2) and why double-booking prevention (open questions #9) is entirely ours. |
| 17 | ✉️ **SENT 2026-08-03, awaiting Alex. Does each property have an application link so tenants can apply anytime?** Brent asked this on 2026-08-03. **Unanswered.** | §1.7's application handoff, and it sharpens ask #8. A standing always-open link makes the handoff a pure per-property config lookup; a conditional one (only when units are available, only during a campaign) means the agent has to know whether the link is live before handing it out. **The only thing actually sent and outstanding**, though it is no longer the only near-term ask (see #7, #18 and #19). |
| 18 | 🆕 **NOT YET ASKED. Enable `sendWorkOrders`** (write, group `maintenance`) **on NPI-owned properties.** New on 2026-08-05 from Brent's decision that the Foothold agent should file resident service tickets itself on properties NPI owns (§3.2). ⚠️ **This has never been requested in any form.** The `maintenance` group landed 2026-08-03 with `getWorkOrders` + `getWorkOrderPickLists` and neither write. ⚠️ **Scope it to owned properties, do not ask for Owyhee, and do not include `updateWorkOrders`.** | Resident service-ticket filing on Aspen and Summerset once live. ⚠️ **A grant is necessary and not sufficient:** rule 2 still has to lift for that property under all three conditions (§4.4), and the resident still has to be verified before a ticket is filed. **Until both gates clear, and permanently at Owyhee, residents go to the Resident Portal**, which already carries 50% of that building's 785 work orders (§3.2). 🔄 **Send `sendLeaseActivities` (#19) in the same email.** It was deferred earlier on 2026-08-05 and Brent reversed that the same day, and one email carrying both beats a second one chasing the first (§2.6). ⏸️ **TABLED 2026-08-13 (Brent, DECISIONS.md session): the email is not sent and stays parked until Brent un-tables it.** |
| 19 | 🆕 **NOT YET ASKED. Enable `sendLeaseActivities`** (write, group `leases`) **on NPI-owned properties.** Added 2026-08-05 when Brent **reversed** his own deferral of it earlier the same day (§2.6). ⚠️ **This has never been requested in any form.** The `leases` grant is `getLeases`, `getLeaseDetails`, `getMitsLeases` only, and the read-back companion `getLeaseActivities` is not on the key either, so we could not verify what we wrote. 🔄 **Send it in the same email as #18**, which is the larger and more consequential grant: one round trip instead of two. **Frame it the way it actually helps Liberty:** it puts Foothold's resident touches in their system **instead of their inbox**, so it reduces their email volume rather than adding to it. ⚠️ **Scope it to owned properties and do not ask for Owyhee.** | Logging Foothold's own resident touches (birthday card sent, pet card sent, feedback received) onto the Entrata record, which is the §2.6 PM-visibility gap. ⚠️ **A grant is necessary and not sufficient, and it is the trap on this row:** it is a **write**, so rule 2 still has to lift for that property under all three conditions plus Brent's written sign-off naming the method (§4.4), and **at Owyhee Park it stays unused even if granted**, because Owyhee is Liberty's building and is not covered by the lift. **Getting the permission is not authorization to call it.** ⚠️ **It covers resident / lease-level touches only**, which is the half that has no other route. ⚠️ **CORRECTED 2026-08-10: this row used to justify itself partly with "an AI leasing chat can never be logged, because no lead-event write method exists anywhere in the 128-method spec." That was wrong and is withdrawn** (§1.4): `updateLeads` r2 and `sendLeads` r1 both append lead events and both are on the key. **The ask stands exactly as written** (resident-level touches are what it is for, and nothing else on the key reaches them), **only that piece of the reasoning was wrong.** On the lead side the open item is the event **type** for a text or a chat, not the existence of a method (Tier 3 #8), and rule 2 still bars the call either way. 🆕 **Strengthened 2026-08-10:** Brent decided the resident follow-up is an AI-generated personal message about that resident's specific issue (§3.2), which adds a fourth touch type to the list above. **A resident-level service touch is exactly what `sendLeaseActivities` would carry.** Until it lands, and permanently at Owyhee, that touch logs only to Foothold's own store. ⏸️ **TABLED 2026-08-13 (Brent, DECISIONS.md session): the email is not sent.** While tabled, resident touches log to Foothold's store and surface in the five-line weekly digest (§1.4). |

## Deliberately NOT asked (recorded so nobody re-adds them without thinking)

Each of these was considered for the 2026-07-31 email and cut on purpose. Add one back only when
something concrete starts depending on it. **Confirmed still absent from the key on the 2026-08-03
probe:** the whole `customers` group (`getCustomers`), `getLeadEvents`, `getPropertyMedia`, and
`getExpiringLeases`. That's the expected result of not asking, not a refusal.

🔄 **One item left this list on 2026-08-05: `sendLeaseActivities`.** It sat here for part of that day
and **Brent reversed the deferral the same day**, so it is now **asks #19** and goes to Liberty in the
same email as #18. Do not re-add it here.

| Left off | Why |
|---|---|
| `getLeadEvents` | The `getLeads` r1 `eventDateFrom` / `eventDateTo` / `eventTypeIds` filters cover the need and return the events inline. Fewer calls is not worth a line of the ask. |
| `getExpiringLeases` | `getLeases` with no status filter carries lease end dates, so expiries are computable. Redundant. |
| `getPropertyMedia` | Not urgent. ILS photo-completeness scoring is a convenience (ask #14). |
| The testimonial methods (`getCustomerTestimonials`, `sendCustomerTestimonials`, `updatePropertyResponse`) | Not urgent. Foothold's review targets are Google and Apartments.com; the Entrata testimonial object is an Entrata-internal surface (§2.5). ✅ **Settled 2026-08-13: skip it permanently; Google + Apartments.com only** (§2.5, Tier 3 #11 closed). |
| `getCustomers` | **Dropped from consideration entirely**, not deferred. Its response schema has **no birth-date field**, so it cannot serve the birthday-card use case even if unblocked, and resident data already arrives via `getLeases` and `getLeads`. |
| `getLeaseDocuments` / `getLeaseDocumentsList` | **Not on the key, confirmed 2026-08-05.** They are the only route to an **executed lease document**, and they were checked while chasing birth dates. **Not needed:** `getLeads` carries the DOB (§2.2), and nothing else in the design wants a lease PDF. **Settled from the document side the same day:** all 44 executed leases in Aspen's DD set contain **zero birth dates** and **one SSN field each**, so this route would buy no DOB and a much heavier PII surface, against the standing no-SSN rule. Adding them would be a **new** ask to Liberty. Ask only if something concrete needs the executed document itself, and never for birthdays. |
| "Uncheck `sendLeads` and `updateLeads`" | ❌ **Declined by Brent 2026-07-31** (ask #5). Closed, not pending. The email was deleted; do not recreate it. |
| `updateWorkOrders` | **Not asked and not in scope.** Ask #18 covers `sendWorkOrders` only. Filing a ticket is the decided capability; **editing or closing Liberty's tickets is not**, and it would make Foothold a second management layer, against "watch the scoreboard, not the plays" (`lead-comms-spec.html` §5, escalation step 3). ⚠️ **That principle is design intent, not a Constitution rule**, so this exclusion rests on a design decision, not a binding gate. It stands as written; if it should be permanent, the principle needs promoting (Tier 3 #19). ✅ **Promoted 2026-08-13: Brent moved "watch the scoreboard, not the plays" into the Constitution** (Tier 3 #19 closed), so the exclusion now rests on a Constitution rule. |

# Asks for Entrata (apisupport@entrata.com)

| # | Ask |
|---|---|
| 1 | **Webhook subscription setup.** Send: a brief description of what we're doing, the event types wanted (`lease.completed`, `lease.started`, `oa.application.events`, `workorder.status.update`), the receiving URL(s), the client name (**Liberty Asset Group**), and the client subdomain (**`libertyassetgroup`**). They respond with the **public key** for signature verification. Liberty ticks the boxes on their side in parallel. |
| 2 | **Which string is actually signed?** The webhooks doc's prose says `Message`, `MessageId`, `Subject` (if present), `Type`, `Timestamp`. Its own sample code concatenates `Message` + `MessageId` + `Type` + `Timestamp` + `TopicArn`. **These disagree.** Get the authoritative answer before writing the verifier. |
| 3 | **Retry semantics.** Retry count, backoff schedule, ordering guarantees, dead-letter behavior. All undocumented. |
| 4 | **Is there a `SubscriptionConfirmation` handshake?** The payload is SNS-shaped but the doc never mentions one. The receiver needs to handle it if it exists. |
| 5 | **Is there any lead-created or lead-updated webhook event** not on the published list of five? |
| 6 | **Is there any sandbox or test company** available for an integration partner, or is live production the only environment? Confirming "no sandbox" in writing is worth having. |
| 7 | **Confirm there is no way to submit application question answers, income, employment, or documents through the API**, and no payment endpoint. We read the spec that way; confirm before anyone scopes work against it. |
| 8 | **Read the "API Security Requirements" and "Secure PII Delivery, Vendor Integration Guide" pages** in the dev portal before any resident data lands in a durable store. (Not an ask, an action item, but it belongs on this list.) |

---

# Open questions, ranked by how much design they block

📌 **Closed 2026-08-13, by the DECISIONS.md session: all twenty open decisions are answered and the
design is LOCKED.** In this list that closes **Tier 1 #2** (FlightDeck: internal-only today, built
toward self-serve software), **Tier 2 #6** (expressed interest: AI judgment behind the hard
minimum-info gate), and **Tier 3 #9, #10, #11, #12, #13, #19, #20, #21 and #22**, and it decides
the **approach** on **Tier 3 #8** (numeric `typeId`, transcript and channel in `comments`, enum-map
fallback) while the `typeId` verification itself stays open, unverifiable without a write. **Tier 1
#1's residue is settled too:** the interim, and permanent-at-Owyhee, PM-visibility path is the
**five-line weekly digest** plus Entrata's own records once the signature lands, with no standalone
page, and the first signature names **`sendLeads` + `updateLeads`** (§4.4). **Tier 2 #5 is
overtaken by events** (Liberty is already onboarding Aspen and Summerset) and now carries the
availability contingency (DECISIONS.md #15): **spec the manual fallback, build it only if Aspen is
not on the key two weeks before go-live.** ⚠️ **None of the twenty answers authorises a write
today. Rule 2 stands.**

📌 **Closed 2026-08-10 by Brent, two decisions, both in Tier 3, and both open new items.** (1)
**Maintenance responsiveness is an AGGREGATE KPI**, which **affirms** "watch the scoreboard, not the
plays" rather than overturning it, and which **adds a metric the design did not have** (§3.1). It is
**dual-purpose by design**: good numbers are marketing proof, bad numbers are an accountability metric
for Liberty, and both uses require one definition, computed the same way every time, never
cherry-picked by window. Its **target is not set** and **whether it joins the six-KPI hero scoreboard
is not decided** (new #20). (2) **The resident follow-up is an AI-generated personal message about the
resident's specific issue**, not the survey `lead-comms-spec` §4 cut, which **stays cut** (§3.2). That
closes **#7** outright. It leaves the **completion trigger and timing** open (new #21) and raises a
question nothing in the corpus had asked: **is the follow-up and the review ask one touch or two?** (new
#22). ⚠️ **Neither decision authorises a write, neither needs a permission, and #19 is unchanged.**

📌 **Closed 2026-08-05 by Brent, three decisions, and one of them moves a Tier 1 item.** (1) **Rule 2
will be lifted on NPI-OWNED properties at go-live**, under three conditions, which answers Tier 1 #1
in principle and leaves a real residue: **Owyhee Park is never covered, so the interim PM-visibility
path is permanent there, not interim.** (2) **The Foothold agent should file resident service tickets
itself on owned properties, after verifying the resident**, which reverses the "never be the intake"
recommendation and adds ask #18 (`sendWorkOrders`, never asked). The **implementation** of verification
is a new Tier 3 question (#18). (3) **`sendLeaseActivities` is a near-term ask** (asks #19, reversed the
same day it was deferred), so the "how does Liberty see our touches" gap now has a route out rather than
staying open on purpose. ⚠️ **The ask does not close it:** the method is not on the key, a grant does not
lift rule 2, and **at Owyhee Park the digest or shared-view path is permanent regardless** (§2.6).
⚠️ **None of these authorises a write today.**

**Closed 2026-08-05, by the third pass:** Tier 2 #4's successor question (**how much of the roll does
`getLeads` actually cover?**) is measured: **65% of current residents, 57 of 88**, with the full
2021-2026 sweep costing **3 calls** on a **730-day** window. ⚠️ **The remaining 35% is not an open
question about Entrata, it is a build requirement on us**: the Welcome Profile is a **required
component**, not a fallback (§2.2). Also closed: **the lead-to-resident join is proven** (same
`customerId` namespace, 70 of 88 matched), and **every method on the key has now been probed**, so
nothing anywhere is waiting on a call. ⚠️ **One new build-critical trap came out of it and it is not
a question, it is a constraint: Entrata's Cloudflare edge 403s a default User-Agent** (§4.2).

**Closed 2026-08-05, by probing the five remaining granted methods:** Tier 2 #4 (**where do resident
birthdays come from?**) is answered: **`getLeads`, not the lease record**, which
narrows the remaining question to coverage for long-tenured residents rather than "is there a source
at all." §1.6's tour-calendar unknown is also closed: the calendar holds **real configuration**, so
availability is designable today even though the booked state stays uncertain. **The two Tier 1
questions are still untouched.** Every 2026-08-05 finding was a read, and none of them moved Brent's
write sign-off or FlightDeck's tenancy model. **No new question came out of the probe**, and nothing
new needs asking of Liberty.

**Also closed 2026-08-05, on the second pass:** §2.3's pet-source gap. `getLeases` returns a
per-resident pet roster **with names** (34 records across 30 of 52 leases), so the pet/no-pet split
and the pet-gift program both have real data, subject to the `isAssistanceAnimal` fair-housing
exclusion. Aspen's executed leases also closed the DOB question from the document side (**zero birth
dates in 44 leases**, §2.2), which retires `getLeaseDocuments` as a route rather than deferring it.
**And one thing arrived that is not a question at all: Brent's binding rule that NPI never ingests,
persists, or logs an SSN** (§2.2). It closes nothing and opens nothing; it constrains everything on
the read path.

**Closed 2026-08-03, by Alex's reply:** Tier 2 #3 (**does Liberty enable `getWorkOrders`?**) is
answered **yes** and the method is verified functional. Tier 3 #14 (escalation channel) and Tier 3
#15 (how Liberty books tours) are both answered. **The two Tier 1 questions are untouched.** The
grant was reads only, so nothing about Brent's write sign-off or FlightDeck's tenancy model moved.
**One new open question and one new idea:** #16, does each property have a standing application link
(Liberty asks #17, sent 2026-08-03, unanswered), and #17, self-service tour scheduling, which Brent
was explicit is **not critical and not a blocker.**

**Closed 2026-07-31:** applications (§1.7) and human escalation (§1.5). Neither is a blocker
anymore. Applications are Entrata's hosted flow and the agent just hands over the URL, so what's
left is one config item (get the URL from Liberty). Escalation is an out-of-band notification plus
an Entrata note, so the leg that reaches a human has no Entrata dependency and the only open piece
is a channel question that went out to Alex Costa on 2026-07-31 (asks #15).

**Also closed 2026-07-31, by declining it:** the planned ask to have Liberty uncheck `sendLeads` and
`updateLeads` (asks #5). Brent is leaving both enabled and accepting the risk. **That removes a
mitigation, not a rule.** Rule 2 still bars every write against a live production property, and the
code-side deny-by-default read-method allowlist is now permanently the only control there will ever
be. Treat write capability on the key as a standing fact, not a temporary condition.

## Tier 1: blocks design now

Two, and only two.

| # | Question | Why it blocks |
|---|---|---|
| 1 | ✅ **ANSWERED IN PRINCIPLE 2026-08-05, and the residue is real. Brent will sign off writes, on NPI-OWNED properties, at go-live.** So replay is **imminent for Aspen and Summerset and indefinite for Owyhee Park**, which is the half that still needs building: **Owyhee is Liberty's building and stays read-only permanently**, so the interim PM-visibility path (a scheduled digest to the property inbox, or a read-only Foothold view) is **not interim there, it is the design.** ⚠️ **Nothing is authorised yet:** the three conditions (NPI-owned, live, signed off in writing naming the property and the methods) must all hold first (§4.4). 📝 **Sharpened 2026-08-13 (Brent, DECISIONS.md session): the first signature names `sendLeads` + `updateLeads` at go-live**, with work orders and lease activities on a second signature later (§4.4), **and the Owyhee-permanent visibility path is decided: the five-line weekly digest, no read-only Foothold page** (§1.4). | The guest card and every communication log are writes. **The mirror-into-Entrata half of Module 1 sits behind that signature**, and until a property clears all three conditions Liberty sees nothing at all there. The outbox, retry, dedupe and replay-ordering choices now have an answer for owned properties and a different one for Owyhee, so **the client has to support both, per-property, from the first line of code.** |
| 2 | **What is FlightDeck's tenancy model, given there is no NPI org?** ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): internal-only today, built toward self-serve software long-term, not a done-for-you services business** (§3.4). | Reshapes the product. Per-PM-org onboarding, PM as a dependency in every sale, property-ID allowlist as a security boundary. Blocks Module 3's product thesis and the FlightDeck data model, not just its wiring. |

## Tier 2: blocks a module or an engine

| # | Question | Why |
|---|---|---|
| 3 | ✅ **ANSWERED YES 2026-08-03. Liberty enabled `getWorkOrders`** (and `getWorkOrderPickLists`), and a read-only probe returned real production work orders (§3.1). | **No longer blocks.** Module 3's monitoring, the turn engine, the review engine's third trigger, and the work-order webhook all have a verified data source. **The successor question is not a permission question:** what counts as a follow-up-worthy ticket, given `maintenanceRequestFrom` separates Entrata-PaaS make-readies from resident-reported work. Still deliberately not decided (§3.2, Tier 3 #7). |
| 4 | ✅ **ANSWERED 2026-08-05: from `getLeads`, not from the lease record**, and ✅ **MEASURED the same day (third pass): it covers 65% of current residents, 57 of 88.** `getMitsLeases` and `getLeases` both return **none**. DOB is captured at the lead / application stage and never propagates. ⚠️ **The "122 of 122" figure is corrected: Entrata omits null fields, so the real numbers are 286 of 787 lead-customers (36%) and 57 of 88 current residents (65%).** The coverage successor question is **closed too**: `getLeads` takes a **730-day** window, so the full sweep costs **3 calls**, and the **join is proven** (same `customerId` namespace, 70 of 88 matched). | **No longer blocks Module 2's birthday card, but it does not fully solve it either.** ⚠️ **31 of 88 current residents have no Entrata birthday and never will** (18 absent from `getLeads`, 13 present without one), so **the Welcome Profile is a required component of the recognition engine, not a fallback.** Rules that carry forward: join on **customer ID**, store **month/day only, never the year**, and **never ingest, persist, or log an SSN** (§2.2, Brent 2026-08-05). Also settled from the document side: **44 Aspen executed leases contain zero birth dates**, so `getLeaseDocuments` is not a DOB route at any property. `getCustomers` does not solve it, was dropped from consideration, and is still absent from the key. |
| 5 | 🔺 **When do Aspen and Summerset land on the key, and what does it take?** ⚠️ **Promoted in weight 2026-08-05:** this is now **the largest unowned dependency before the 8/31 close** (asks #7). 📌 **OVERTAKEN BY EVENTS 2026-08-13: Liberty is already working on onboarding both properties**; no email needed, monitor progress. 🧰 **And the contingency is decided (DECISIONS.md #15): SPEC the manual availability fallback now, do not build it, with a named trigger: if Aspen is not on the key two weeks before go-live, build it.** | Module 2 has no NPI-owned property to run against. Every engine needing an owned building waits. **And since the rule 2 lift is NPI-owned-only, this is now the gate on the entire write half of the design**, not just Module 2's fulfillment. Note Aspen still does **not** inherit writes on arrival: it has to be live and signed off in writing as well (§4.4). |
| 6 | **What machine-checkable test means "expressed interest in a particular property"?** ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): AI judgment is fine, behind the HARD minimum-info gate: (phone OR email) AND at least a first name. No guest card without a way to follow up** (§1.2). | It's the trigger for the whole dividing line. Needs to be more specific than the minimum-info gate. |

## Tier 3: shapes a feature, doesn't block it

| # | Question |
|---|---|
| 7 | ✅ **ANSWERED 2026-08-10 (Brent), both halves. It is the lighter personal check-in, AI-generated off the resident's specific issue** (*"Did your back door get fixed?"*), **not the survey**, and the `lead-comms-spec` §4 cut **stays cut** (§3.2). **And it does not cross the "watch the scoreboard" line, because the message goes to the RESIDENT, not to Liberty**: nothing per-ticket is sent to, demanded of, or logged against Liberty, and per-ticket outbound to a resident is a different act from per-ticket policing of a vendor. ⚠️ **New rule that came with it:** the AI renders from the actual field values and **never invents detail**; if problem plus location will not form a natural, confident sentence, fall back to a generic check-in or do not send. 🚫 **NPI-owned properties only, never Owyhee.** What is still open moved to #21 and #22. |
| 8 | **How do texts and chat messages get logged**, given `updateLeads` r2's enum has no Note/text type? Numeric `typeId`, `sendLeads` r1 instead, or approximate mapping. Unverifiable without a write. 🔺 **LIVE AGAIN as of 2026-08-10, and it had effectively gone moot for the wrong reason.** §1.4 item 4 used to declare lead-level logging permanently impossible, which made this question pointless. **That claim was wrong and is withdrawn**: `updateLeads` r2 and `sendLeads` r1 both carry an `events` array with `typeId`, `date`, `type` and a free-text `comments` field, and both are on the key. **So the mechanism exists and this is the actual open question about it.** The candidate answer is a numeric `typeId` resolved from `getLeadPickLists` (155 event types, readable today, so pin the vocabulary now), and `sendMitsLeads` r1's enum proves Entrata's model has `Note` / `IncomingText` / `OutgoingText` somewhere. ⚠️ **Still unverifiable without issuing a write, and rule 2 bars that. Do not learn this by testing against a live property.** 📝 **APPROACH DECIDED 2026-08-13 (Brent, DECISIONS.md session): build against the numeric `typeId` off `getLeadPickLists`** (pull the 155 IDs now, while reads are all that is allowed), one-line summary on top, **full transcript and true channel in the free-text `comments`**, enum-map as the runtime fallback, never learned by a test write; if a full transcript bounces on an undocumented `comments` length cap at go-live, fall back to summary-only. ⚠️ **The verification itself REMAINS OPEN: which `typeId` is right for a text or a chat cannot be verified without issuing a write.** |
| 9 | **Who owns double-booking prevention for tours**, and where does that state live? The API almost certainly won't do it for us. ⚠️ **Sharpened 2026-08-05:** the availability config is real and readable (limit **1** for self-guided, 30-min appointments, 120-min lead time), so the parameters are no longer guesses, but a manually booked agent tour may still never appear in Entrata, so the reconciliation problem is unchanged (§1.6). ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): propose for agent-guided, commit for self-guided** (§1.6). Foothold owns and commits the self-guided booking state; agent-guided is propose-only because Liberty books manually. |
| 10 | **Do we sync Entrata-side opt-outs into GHL?** ⚠️ **Now concrete rather than theoretical:** `getMarketingPreferences` is VERIFIED working and returns `optIn` per consent type per channel, with customer IDs sourced from `getMitsLeases` (§1.3). Consent is readable and unsynced today, and it should be a **gate on the send path**, not a report. ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): BOTH.** The live check on the send path is the load-bearing gate, a nightly sync into GHL runs as hygiene, and a missing preference means do-not-send (§1.8). |
| 11 | **Do we use the Entrata testimonial object at all**, or only Google and Apartments.com? Low stakes. ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): skip it. Google + Apartments.com only** (§2.5). |
| 12 | **After-hours handoff:** the agent holds the conversation, or an explicit business-hours queue? (Carried from `system-design.html` §14, unchanged.) ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): hybrid.** The agent holds by default; an emergency or an explicit "I want a person now" escalates immediately, and an emergency gets the phone number handed over on the spot, never a queued callback (§1.5). |
| 13 | **Webhooks vs. polling for lease events**, once webhooks are enabled. Availability polling stays regardless. ✅ **ANSWERED 2026-08-13 in practice (Brent, DECISIONS.md session): provision webhooks now, bundled into the Aspen/Summerset onboarding, and observe what arrives at Owyhee read-only** (§4.6). Availability keeps polling, and the work-order follow-up trigger polls by decision (§3.1). |
| 14 | ✅ **ANSWERED 2026-08-03: email Liberty's direct leasing / manager team** (`adrienne@libertyassetgroup.com` today). They prefer email over their Google / Breakroom chats. ⚠️ **The team is being restructured, so the recipient is per-property config, not a constant.** Channel question closed; the two-leg escalation design was already settled (§1.5). |
| 15 | ✅ **ANSWERED 2026-08-03: Liberty books tours manually.** Agents schedule them personally around their own drive time, or hand out lock box info for a self-guided tour. Reshapes §1.6 and makes #9 (double-booking ownership) entirely ours, since a manually booked tour may never appear in Entrata at all. |
| 16 | ✉️ **OPEN, sent to Alex 2026-08-03: does each property have an application link so tenants can apply anytime?** Feeds §1.7 and Liberty asks #8 / #17. A standing always-open link makes the handoff a config lookup; a conditional one means the agent has to know when the link is live before handing it out. **Unanswered.** |
| 17 | **Self-service tour scheduling: build it or not?** Brent, 2026-08-03: worth exploring later. A Calendly-style surface with drive-time buffers on either side of a slot, and an AI agent nudging leads into adjacent slots so an agent's day clusters geographically. **He was explicit that it is NOT critical and NOT a blocker.** Recorded so it isn't lost or mistaken for a requirement (§1.6, gap 6). |
| 18 | **How does resident verification actually get implemented on the ticket-filing path?** The policy is decided (§3.2: phone on file first, unit plus one more factor next, DOB never as the only gate, fail closed to a human). What is **not** decided is the mechanics: how a match is scored, how long a verified session lasts, what the handoff to a human looks like when it fails, and how a resident who texts from a new number gets re-verified without friction. ⚠️ **This shapes the feature, it does not block it**, because nothing can be filed until `sendWorkOrders` lands and rule 2 lifts anyway. **Do not let "we'll verify" stand in for a written rule.** |
| 19 | **Should "watch the scoreboard, not the plays" be promoted into the Constitution?** ⚠️ **Sharpened, but NOT closed, on 2026-08-10.** Brent **affirmed the principle** that day by choosing an aggregate maintenance KPI over per-ticket policing (§3.1), so it is no longer an unattributed design-intent line: it is a decision he has made, with a date on it. **What has not changed is its status.** It still lives in `lead-comms-spec.html` §5 (escalation step 3) as **design intent, not a Constitution rule**, so it still does not carry the "a build that violates one is wrong even if it works" force, and nothing stops a future build from crossing it. It is load-bearing in five places now: that step, its "why the restraint" callout, the §3.2 confirmation resolution, the permanent exclusion of `updateWorkOrders` from ask #18, and the aggregate-only shape of the §3.1 metric. It was miscited as "Constitution rule 5" three times before being corrected. **Promoting it is still Brent's call, not the document's.** ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): PROMOTED into the Constitution.** |
| 20 | 🆕 **The maintenance-responsiveness metric has no target and no home.** The metric itself is **DECIDED** (§3.1, aggregate, dual-purpose, built off `getWorkOrders`), but two things were deliberately left open. (a) **No target or threshold is set.** ⚠️ The existing **24hr / 4hr** number is a **FIRST CONTACT** target, parked at Phase 2 pending Liberty's commitment, and is **not a resolution target**; do not conflate them. (b) **Whether it joins the six-KPI hero scoreboard on `index.html` or stays a feeder line on `project-plan.html` is not decided.** ⚠️ Because the number is dual-purpose (marketing proof when good, Liberty accountability when bad), **the definition has to be fixed in writing before either use**, and it cannot be redefined by window afterwards. ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session), both halves: NO target, and it goes ON the scoreboard.** Track it for a while and display it; no SLA negotiation with Liberty unless the data eventually says there should be one (§3.1). |
| 21 | 🆕 **What triggers the resident follow-up, and when?** The message is **DECIDED** (§3.2) and the trigger is not. Two sub-questions: **poll `getWorkOrders` versus the `workorder.status.update` webhook** (poll works today with no provisioning; push is cleaner but needs the two-gate provisioning, §4.6), and **which `maintenanceStatus` values count as "complete enough to ask"** (statuses are client-defined; `getWorkOrderPickLists` returns Liberty's real `StatusTypes`, so this is a lookup rather than a guess). **Timing after completion is also unset.** ⚠️ Whatever is chosen inherits the §3.2 filters: skip `Entrata PaaS`, collapse parent/child, clear consent on both lenses. ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): POLL**, with the qualifying statuses named in config off Liberty's real `StatusTypes`, sent **next business morning**, plus an **AI-judgment gate on which work orders deserve a follow-up at all**, its rules worked out against real Owyhee examples during build-spec (§3.1, §3.2). |
| 22 | 🆕 **Is the resident follow-up and the review ask ONE touch or TWO?** Raised 2026-08-10 and **not decided.** The §3.2 follow-up ("did your back door get fixed?") and the §2.5 work-order-complete review ask fire off the **same event**, at the **same resident**, through the **same channel**. Nothing in the corpus has ever addressed whether they are one message or two, or in what order. ⚠️ Two things bound it: rule 7 says review asks trigger on events and go to **everyone** at that event, with **no sentiment gating**, so a follow-up must never become a filter in front of the ask; and `lead-comms-spec` §6's "don't spam" rule says space the touches so it never reads like a campaign. ✅ **ANSWERED 2026-08-13 (Brent, DECISIONS.md session): TWO messages**, follow-up first, review ask a few days behind, **fired on the event, never on the reply** (§2.5). |

---

## Provenance

| Date | What |
|---|---|
| 2026-07-31 | Live read-only probe of Liberty's production org: 22 services permissioned, 16 returning data. Full OpenAPI spec analysis (18 groups, 128 methods, 302 schemas, 63,848 lines). Webhooks doc captured from the dev portal. **No writes were made and none are authorized.** |
| 2026-07-31 | Brent's design intent for the three modules stated and captured here verbatim in the INTENT blocks. |
| 2026-07-31 | **Two gaps closed by Brent.** (1) **Applications (§1.7):** the AI agent never collects application data; it hands over Liberty's hosted ProspectPortal URL and the prospect applies there. API-submitted applications were never the intent, so §1.7 is no longer a gap. What's left is a per-property config item (get the URL from Liberty, ask #8). (2) **Escalation (§1.5):** the signal to Liberty doesn't have to be an Entrata object. It's an out-of-band notification they actually monitor (property inbox email, Slack, or similar) **plus** a corresponding note on the Entrata lead record. The channel question goes to Liberty and was deferred to escalation design (ask #15). Neither is a top-tier blocker anymore. **[Superseded later the same day: Brent asked the channel question now. See the row below.]** |
| 2026-07-31 | **Brent declined the write-permission cleanup. Closed, not pending.** The planned ask to Liberty ("uncheck `sendLeads` and `updateLeads` on the API user") **will not be made.** Brent is accepting the risk and leaving both write permissions enabled on the key. His reasoning: the permission being enabled is not what causes a write, our code calling a write method is, and it isn't worth a second round-trip with the PM. **This does not change Constitution rule 2.** We still never write to a live production property without his written per-property sign-off naming the property and the methods. What changes is only the mitigation posture: **there will be no vendor-side guard, permanently and by choice, so the code-side deny-by-default read-method allowlist is the sole control and always will be.** Recorded here so nobody re-raises the ask in three months. Asks list #5. |
| 2026-07-31 | **Two questions moved from deferred to in-flight with Alex Costa at Liberty.** (1) The escalation question ("if a prospect wants a person, what's the best way to get that in front of your team?") had been filed ASK LATER; Brent asked it now (asks #15, §1.5). (2) **New question:** how does Liberty book tours today, so §1.6's scheduling design lines up with their real mechanic rather than a guess (asks #16). Both went out in the same email. |
| 2026-07-31 | **The 2026-07-30 retraction draft was deleted.** Never sent, no longer exists. Every description of it as "sitting unsent in Gmail drafts" was stale and is corrected. **No pending email, and nobody should recreate one.** |
| 2026-07-31 | ✉️ **The permission asks were SENT.** Brent emailed Alex Costa (cc Taylor Grange), "Entrata API user for Liberty" thread, asking for `getWorkOrders` + `getWorkOrderPickLists` (maintenance, asks #1), `getMitsLeases` (leases, asks #2), `getCalendarAvailability` (properties) and `getMarketingPreferences` + `getMarketingPreferencePickList` (communications) (asks #3), plus the two operating questions (asks #15, #16). **In flight, awaiting his reply.** Deliberately left off, with reasons now recorded in a dedicated block after the asks table: `getLeadEvents`, `getExpiringLeases`, `getPropertyMedia`, the testimonial methods, and `getCustomers` (dropped outright, no birth-date field). **[SUPERSEDED 2026-08-03: every item in this email was granted or answered. Nothing here is still in flight. See the rows below.]** |
| 2026-08-03 | ✅ **The second grant landed. The key went from 22 services to 28.** A read-only `getAccessibleServices` probe from the VPS (`5.78.139.104`) returned HTTP 200 with **28 services**. **All six methods asked for on 2026-07-31 were granted, all reads:** `getWorkOrders` + `getWorkOrderPickLists` (group `maintenance`), `getCalendarAvailability` (group `properties`), `getMitsLeases` (group `leases`), `getMarketingPreferences` + `getMarketingPreferencePickList` (group `communications`). **The `maintenance` and `communications` groups are no longer absent / 1403.** Alex Costa (VP Ops, Liberty) made the grant. **WRITES ARE UNCHANGED at exactly two: `sendLeads` and `updateLeads`.** `sendWorkOrders` and `updateWorkOrders` are **still not on the key even though the `maintenance` group now is**, so that is a **deliberate per-method state, not a group side effect**, and the group's arrival must not be read as the writes being close behind. **Constitution rule 2 is unchanged and undiluted:** no writes against a live production property, ever, without Brent's written per-property sign-off. Still absent from the key: the whole `customers` group (`getCustomers`), `getLeadEvents`, `getPropertyMedia`, `getExpiringLeases`. |
| 2026-08-03 | ✅ **`getWorkOrders` VERIFIED FUNCTIONAL, not merely permissioned.** A read-only r1 call with `propertyId` `1311115` returned HTTP 200 and real production work orders: `maintenanceRequestId` 3435 and 3436, `buildingName` "3030", `unitNumber` "3030-201", `status` "Completed", `problem` "Make Ready", `maintenanceRequestFrom` "Entrata PaaS", MDT timestamps, and parent / child linkage via `parentMaintenanceRequestId`. **The turn engine's make-ready signal is in the data by name**, and §3.1's gap is resolved. Two design consequences recorded there: filter on `maintenanceRequestFrom` so system-generated make-readies never trigger resident surveys, and collapse to the parent request before counting or messaging. ⚠️ **The other five newly granted methods were NOT individually probed.** They are permissioned and nothing more; **never state what they return.** **[SUPERSEDED 2026-08-05: all five were probed read-only and all five work. See the rows below.]** **No writes were made and none are authorized.** |
| 2026-08-03 | ✅ **Alex answered both operating questions** (email, 08:47 AM PT). (1) **Tours are booked MANUALLY.** Agents take a call or an email and schedule the tour themselves, because each manages a portfolio and has to arrange drive time, or they hand out lock box information for a self-guided tour. ⚠️ **Design implication: `getCalendarAvailability` is permissioned but Liberty does not appear to run tour scheduling through Entrata's calendar, so it must be validated against real data before any design depends on it** (§1.6). **[PARTLY SUPERSEDED 2026-08-05: it was validated, and the calendar holds real configuration, including self-guided tours seven days a week. What stays uncertain is only whether manually booked agent tours land there.]** (2) **Human escalation goes by email** to Liberty's direct leasing / manager team, `adrienne@libertyassetgroup.com` today; they have Google / Breakroom chats but prefer email, and **the team is being restructured, so treat the recipient as per-property config, not a constant** (§1.5). Liberty asks #15 and #16 both closed. |
| 2026-08-03 | **Brent, 2:41 PM PT.** (1) ✉️ **New open question to Alex, unanswered: does each property have an application link so tenants can apply anytime?** (Liberty asks #17; feeds §1.7 and sharpens ask #8.) It is the only thing in flight with Liberty. (2) **Recorded as an idea, explicitly NOT critical and NOT a blocker: self-service tour scheduling.** Calendly-style, with drive-time buffers on either side of a slot and an AI agent nudging leads into adjacent slots so a leasing agent's day clusters geographically instead of scattering. To explore later, not scope (§1.6 gap 6; open questions Tier 3 #17). |
| 2026-08-05 | ✅ **All five remaining granted methods PROBED read-only. All five returned HTTP 200 and real data, so nothing carries "PERMISSIONED, UNPROBED" anymore.** (1) **`getWorkOrderPickLists`**: property-scoped `Locations`, `Priorities`, `StatusTypes`, `Categories`, `Problems`, `SubMaintenanceProblems`, plus `WorkOrderTypes` (5), `InspectionStatuses` (5), and **`CompanyEmployees` (12), Liberty's actual staff roster**, which makes work-order routing to a named human expressible on reads alone (§3.1). It does **not** unblock an assignment write; `sendWorkOrders` / `updateWorkOrders` are still off the key and barred by rule 2. (2) **`getMitsLeases`**: works, 100 `Tenant` vs 52 `LA_Lease`, adds `Finances`, `GuarantorFor`, `Spouse`, and emergency contacts, and supplies the customer IDs `getMarketingPreferences` needs. ⚠️ **No birth dates.** (3) **`getCalendarAvailability`**: returns **real configuration**: 30-minute appointments, 120-minute minimum lead time, agent-guided **Mon-Fri 08:00-16:00 MST**, **self-guided all seven days 06:00-18:00 MST at `simultaneousAppointmentLimit` 1**, plus per-date available hours. ⚠️ **The live window cap is 7 days, not the 60 the spec implies**; 8 or wider returns HTTP 400, code 308. This **reverses** the 2026-08-03 expectation that the calendar would be empty, and the self-guided config corroborates Alex's lock-box answer (§1.6). (4) **`getMarketingPreferencePickList`**: 24 rows, LEAD(1) / RESIDENT(2), channels email / phone / postal_mail / sms. ⚠️ **Tightest rate limit on the key: 1,500/day, 60/hour, 10/minute. Cache it.** (5) **`getMarketingPreferences`**: works with `recipientTypeId=2`, returns `optIn` per consent type per channel. **The `customerIds` blocker is resolved.** The IDs come off `getMitsLeases` (`IDType: "Customer ID"`), so consent becomes **a real gate in front of any resident or lead messaging** (§1.3, §1.8). **No writes were made and none are authorized. Constitution rule 2 is unchanged.** |
| 2026-08-05 | 🎂 **Resident birth dates SOLVED, and not where the design expected.** `getLeases` r1 + r2 return **zero** birth keys and `getMitsLeases` returns **zero**; the field is simply empty in Liberty's lease data. But **`getLeads` r1 returns `birthDate`** (requires `propertyId` + `fromDate` + `toDate`). ⚠️ **CORRECTED by the third pass the same day: the "POPULATED, 122 non-empty of 122" reading recorded here was an artifact of Entrata omitting null fields and it was wrong. Measured: 286 of 787 lead-customers (36%), covering 57 of 88 current residents (65%). See the third-pass rows below.** **DOB is captured at the LEAD / APPLICATION stage and never reaches the lease or resident record**, which is Entrata's data model rather than a Liberty data-entry gap. **Design path:** pull DOB from `getLeads` and join to the resident on **customer ID**. **Coverage caveat:** only residents with a lead record inside the queried window are covered, so long-tenured residents need a historical sweep and the Welcome Profile stays the fallback. ⚠️ **Superseded by the third pass: the sweep was run (3 calls, 730-day window), coverage measured at 65%, and the Welcome Profile was promoted from fallback to REQUIRED component.** ⚠️ **PII rule: store month and day only, drop the year.** ⚠️ **`getLeaseDocuments` / `getLeaseDocumentsList` are NOT on the key.** They are the only route to an executed lease document, and it would take a **new** Liberty ask. **Not needed now.** This **reverses** the position held since 2026-07-31 that a resident's birthday had no Entrata source (§2.2; open questions Tier 2 #4). |
| 2026-08-05 | 🚨 **NEW BINDING DATA RULE FROM BRENT: NPI never ingests, persists, or logs an SSN.** He has no use for it and does not want the liability. **This is a decision, not a finding.** What the probe established around it: SSN / `taxId` appears in the **response** schemas of `getMitsCollections` r1, `getVendors` r1, and **`getMitsLeases` r1 (`SocialSecurity`), which IS on our key**, and the live 2026-08-05 `getMitsLeases` call returned **no SSN keys**, so Liberty leaves the field empty and **there is no exposure today.** The rule stands regardless, because the field can start populating without warning. **Enforcement: the single request choke point that already carries the read-method allowlist must also strip `SocialSecurity` / `SSN` / `taxId` on read, before anything is persisted, logged, or passed to an AI model.** Fail-safe, at the transport layer, never a downstream cleanup pass. **Paired with §2.2's month-and-day-only birth-date rule, these are the two standing PII-minimization rules**, and they govern lease-document and DD ingestion as well as the API, since Aspen's executed leases each carry an SSN field. Recorded in §1.7, §2.2, §3.3, and §3.4. |
| 2026-08-05 | 🐾 **Pet data probed live, and it unblocks the pet-gift engine.** **`getLeases` r1 is the source:** **30 of 52 leases have pets, 34 individual pet records**, each with **`name`**, `petType`, `breed`, `color`, `gender`, `age`, `weight`, `moveInDate`, `customerId`, and `isAssistanceAnimal`. `getLeads` r1 carries **126 pet records** (adds `count`, `id`, `licenseCity`), so pets are known at guest-card time. `getMitsLeases` r1 carries only `Count`, `PetType`, `Weight` and **no name**, so it is not the pet source. `getPetTypes` is not on the key and **is not needed**, `petType` comes inline. `getWorkOrders` r1 carries `petInfo`, useful for warning a technician about an animal in the unit (§3.1). **Pets join to the resident on `customerId`**, the same key as the birthday join and the consent lookup. ⚠️ **`isAssistanceAnimal` is load-bearing: assistance and service animals are NOT pets under fair housing, so they are a hard exclude from the pet-gift program and from any pet-themed marketing. Fair-housing risk, not a tact preference.** This closes §2.3's pet-source gap, which had stood since 2026-07-31, and gives the ops repo's existing `pet-gift.html` mockup a real data source. |
| 2026-08-05 | 📄 **Aspen executed-lease scan: the document-side counter-example that closes the DOB question.** All **44 leases** in the Aspen DD set (`00_Deals/Boise - Aspen/10_Deal_Shared/PM_Shared/Seller_DD/02. Lease and Tenant Info/a. Lease & Rental Agreements/Leases/`) were text-extracted, **7.4M characters, no OCR needed**. Result: **zero occurrences of "date of birth" or "birth"**, one SSN field per lease, and one "18 years" adult-occupant clause per lease. 21 of the 44 carry `breed:` / `weight:` labels, so pets are documented on paper, but **no pet-name label**, which makes Entrata the better pet source too. **Executed leases record the *result* of screening, never the applicant's DOB.** This confirms the API finding from the opposite direction and means **`getLeaseDocuments` should not be pursued for DOB at any property** (§2.2, §4.3, "Deliberately NOT asked"). It also demonstrates the no-SSN rule's reach: the lease *documents* carry SSNs, so document and DD ingestion falls under the same rule as the API. |
| 2026-08-05 (third pass) | 🎂 **DOB COVERAGE MEASURED, and the earlier count was wrong.** ⚠️ **CORRECTION FIRST: `getLeads` `birthDate` was never "populated 122 of 122."** Entrata **omits null fields** from its responses, so that count only measured records where the key already existed. It was circular. **Full sweep, 2021-01-01 through 2026-08-05: 787 distinct lead-customers, 286 carrying a `birthDate`, which is 36%.** Measured against the roll: **103 customer records across 52 leases, 88 `Current`; 70 of the 88 appear in `getLeads`; 57 of the 88 carry a `birthDate`. DOB coverage is 65% of current residents.** The 31-resident gap splits two ways: **18 never appear in `getLeads` at all, 13 appear with no birth date.** ⚠️ **Design consequence, and it is not optional: the Welcome Profile is a REQUIRED component of the recognition engine, not a fallback**, because the remaining third of the roll has no Entrata birthday and never will. **Do not describe birthdays as solved.** ✅ **The lead-to-resident join is PROVEN:** `getLeads` `customerId` and `getLeases` customer `id` are the **same namespace**, 70 of 88 matched, closing the earlier concern that they might be different ID spaces. ✅ **Backfill is cheap:** `getLeads` accepts a **730-day** window, so the whole 5.5-year history swept in **3 calls**; earliest `moveInDate` on the roll is **2022-06-16**. ⚠️ **Window caps are per method and never generalize:** 730 days here versus `getCalendarAvailability`'s 7 (§1.6). Recorded in §2.2, §4.2, and open questions Tier 2 #4. **No writes were made and none are authorized.** |
| 2026-08-05 (third pass) | 🚨 **NEW BUILD-CRITICAL TRAP: Entrata's edge is Cloudflare and it 403s a default User-Agent.** A Python `urllib` call on its **default UA** returned **HTTP 403 with the body `error code: 1010`**, a **Cloudflare browser-signature ban**, not an Entrata JSON error and not a permission failure. The **identical request through `curl` worked.** ⚠️ **The real client MUST set an explicit User-Agent on every request**, and its error handling must treat a **non-JSON 403 body as an edge block**, categorically distinct from a 400 / 1403 permission denial or an auth failure. This will bite whoever writes the client first, because the symptom looks exactly like a revoked key. Recorded in §4.2. |
| 2026-08-05 (third pass) | ✅ **EVERYTHING ON THE KEY IS NOW PROBED. Nothing anywhere is waiting on a call.** The last unprobed methods, all read-only: the three oauth methods (`getAccessToken`, `getJwtAccessToken`, `getUserInfo`) return **403 / error 1403**; `getAppAdditionalServices` returns **400 / error 107** because it does not support apikey auth; `getAccessibleClients` returns **200**, re-confirming subdomain `libertyassetgroup` and **org id 19530**; `getStatus` returns **200** and is a **free liveness check** needing no permissions; **`getLeaseDetails` r2 works with `propertyId` alone**, no lease ID; and **`getMarketingPreferences` works on the LEAD side too (`recipientTypeId=1`), with the same customer able to hold BOTH a LEAD and a RESIDENT preference set at once**, so a consent check has to name which relationship it is testing (§1.3). None of the failures blocks anything: the gateway takes `X-Api-Key` directly. **No writes were made and none are authorized.** |
| 2026-08-05 (third pass) | 📊 **Rate limits are COMPLETE, which closes the standing note that the work-order methods had no measured limit.** `getWorkOrders` **25,000/day · 1,500/hr · 150/min**; **`getWorkOrderPickLists` 1,000/day · 500/hr · 60/min, the tightest DAILY bucket on the key**; `getLeads` **27,000 · 5,000 · 135**; `getLeases` **64,320/hr · 1,072/min**; `getLeaseDetails` r2 **50,000 · 5,000 · 1,000**. With the four captured earlier the same day (`getMitsLeases`, `getCalendarAvailability`, `getMarketingPreferences`, `getMarketingPreferencePickList`), every method on the key now has a number. ⚠️ **Correction to a claim repeated throughout this doc: `getMarketingPreferencePickList` is not the tightest bucket outright.** It holds the tightest **hourly and per-minute** limits (60/hr, 10/min); `getWorkOrderPickLists` holds the tightest **daily** one (1,000/day). **There are two constrained methods, they are constrained differently, and both are vocabularies that must be cached.** Full table in §4.2. |

| 2026-08-05 (decisions) | 🔓 **BRENT'S DECISION: rule 2 WILL be lifted, on NPI-OWNED properties, at go-live. It is not lifted today and nothing is authorised yet.** The read-only rule comes off **NPI-owned** properties when Foothold goes live on them (Aspen, then Summerset). **Three conditions must all hold before a single write is issued: (1) NPI owns the property, (2) Foothold is live on it, (3) Brent has signed off in writing naming that property and those methods.** ⚠️ **Owyhee Park is NOT covered and is not expected to be.** It is Liberty's building; a property NPI does not own stays read-only regardless of permissions, and **a working probe is not a licence to write.** ⚠️ **Build consequence: the deny-by-default allowlist becomes PER-PROPERTY rather than disappearing.** Same choke point, same fail-safe, but a named property can carry a named set of permitted write methods. **A missing, unparseable or unrecognised property config still means read-only, and it gets built that way from the first line of client code.** Everything else about rule 2 stands: `sendLeads` and `updateLeads` are permissioned and still never called, Brent declined to have them removed, and there is no vendor-side guard. Recorded in §4.4, open questions Tier 1 #1, and the decision block at the top. |
| 2026-08-05 (decisions) | 🔧 **BRENT'S DECISION: the Foothold agent SHOULD file service tickets on NPI-owned properties, with verification. This reverses the "Foothold must never be the intake" recommendation.** Target state: on owned properties, once live, the agent files the work order itself when a resident reaches out. **Two hard gates first:** `sendWorkOrders` is **not on the key** and is a **new ask that has never been made** (the `maintenance` group landed 2026-08-03 with two reads and neither write, asks #18), and **rule 2 must be lifted for that property.** ⚠️ **The resident MUST be verified before a ticket is filed**, because an unverified ticket sends a real technician to a real unit: **phone on file is strongest and cheapest** (`getLeases` customer `phone`, `getMitsLeases` `Phone`), **unit number plus one more factor** for an unrecognised number, and **DOB is a weak primary that must never be the only gate because coverage is 65%.** **Fail closed: no match means hand off to a human.** ⚠️ **Until both gates clear, and permanently at Owyhee Park, point residents at the Resident Portal and do not take the request.** Measured the same day across Owyhee's **785 work orders: 390 (50%) already originate in the Resident Portal**, 388 are staff-entered, 7 are system make-readies, and **all 71 work-order locations are resident-portal-enabled.** ⏰ **The after-hours problem survives the change:** an 11pm Friday ticket still waits for Monday against M-F 9:00-5:00 office hours with no weekend coverage, so **genuine emergencies get the phone path, never a chat handoff and never an email.** Two build facts recorded for every maintenance engine: **priority is effectively unused (780 of 785 are `Medium`)**, so triage on `maintenanceProblem` / `maintenanceLocation`; and **312 of 785 are child tickets**, so collapse parent/child before counting or sending. §3.1, §3.2, §4.3, §4.4, asks #18, open questions Tier 3 #18. |
| 2026-08-05 (decisions) | ⏸️ **BRENT'S DECISION: `sendLeaseActivities` is DEFERRED. Do not raise it with Liberty now.** Logging Foothold's own touches (birthday card sent, pet card sent, feedback received) into Entrata is parked as a future ask, and it must not be bundled into the `sendWorkOrders` ask. **The underlying finding stays on the record:** it is the **only method in the 128 that appends an activity** to a record, it is **not on the key**, and **lead-level events (an AI leasing chat) can NEVER be logged, because no lead-event write method exists anywhere in the spec.** So the near-term asks to Liberty are **Aspen + Summerset onto the key (#7), the per-property application link (#17), and `sendWorkOrders` for owned properties (#18)**, and not this. §1.4, §2.6, §4.3, §4.4, "Deliberately NOT asked". **[REVERSED BY BRENT THE SAME DAY. Superseded by the row below: `sendLeaseActivities` IS a near-term ask (#19) and it ships in the same email as #18. Kept for the reasoning trail.]** ⚠️ **[CORRECTED 2026-08-10, and this is a factual correction to the row, not a reversal of the decision. Of the two findings quoted here, only-method-that-appends SURVIVES (scoped correctly it reads "the only method that appends an activity to a RESIDENT / lease record"); no-lead-event-write-ever is WRONG and is withdrawn. `updateLeads` r2 and `sendLeads` r1 both carry an `events` array with a free-text `comments` field and both are on the key. Row kept verbatim for the trail.]** |
| 2026-08-05 (decisions, reversal) | 🔄 **BRENT REVERSED HIS OWN DEFERRAL THE SAME DAY: `sendLeaseActivities` IS a near-term ask to Liberty.** It moves out of "Deliberately NOT asked" and becomes **asks #19**, sent **in the same email as #18**, not a second one. **The reasoning:** we are already asking Liberty for `sendWorkOrders`, which is the larger and more consequential write grant, so bundling the low-blast-radius activity note into that same conversation is one round trip instead of two, and the standing lesson in this corpus is one email with everything on it rather than a second chasing the first. **Frame it to Liberty the way it actually helps them:** it puts Foothold's touches in their system **instead of their inbox**, so it reduces their email volume rather than adding to it. **So the near-term asks are FOUR: Aspen + Summerset onto the key (#7), the per-property application link (#17), `sendWorkOrders` for owned properties (#18), and `sendLeaseActivities` (#19).** ⚠️ **Two things the reversal does NOT change.** (1) **Getting the permission is not authorization to use it.** It is a write, so rule 2 governs it: the per-property lift plus Brent's written sign-off naming the property and the method, and **at Owyhee Park it stays unused even if granted**, because Owyhee is Liberty's building and is not covered by the lift. (2) **Lead-level logging is still permanently impossible.** There is **no lead-event write method anywhere in the 128-method spec**, so an AI leasing chat can never be logged into Entrata; **`sendLeaseActivities` covers resident / lease-level touches only.** §1.4, §1.5, §2.6, §4.3, §4.4, asks #18 / #19, open questions Tier 1 #1. ⚠️ **[CORRECTED 2026-08-10. Point (2) of this row is WRONG and is withdrawn. A lead-event write mechanism does exist and is on the key: `updateLeads` r2 and `sendLeads` r1 both carry an `events` array with `typeId`, `date`, `type` and a free-text `comments` field. The open item is the event TYPE for a text or a chat (Tier 3 #8), not the existence of a method. Point (1) stands untouched, and so does the decision: `sendLeaseActivities` is still the ask, still for resident-level touches, still a write that rule 2 gates. Row kept verbatim for the trail.]** |
| 2026-08-10 (decisions) | 📊 **BRENT'S DECISION: maintenance responsiveness is an AGGREGATE KPI, and it serves two purposes.** He agreed with aggregate over per-ticket policing, which **AFFIRMS "watch the scoreboard, not the plays"** (`lead-comms-spec.html` §5, escalation step 3) rather than overturning it, and **raises that line from unattributed design intent to a decision with a date and a name on it.** ⚠️ **What is new: a maintenance responsiveness metric SHOULD now be built.** The design carried no resolution-time metric at all, and `lead-comms-spec.html` said "no nightly SLA-policing." **Both are true and the reconciliation is plain: measuring the aggregate is not policing; alerting on or chasing an individual ticket still is.** The no-policing line is **qualified, not removed.** ⚠️ **Dual-purpose, in Brent's words:** *"If that KPI is genuinely good we can use it in marketing, and if it is bad then it is an accountability metric for LAG."* **Both uses demand the same discipline: one definition, written down once, computed the same way every time, never cherry-picked by window. A number used in both directions cannot be redefined when it is inconvenient.** **How it computes, and it needs no new permission:** `getWorkOrders` carries `completedOn` (**OBSERVED** 2026-08-05) and `createdOn` (**SPEC**, not yet observed), plus `createdOnFromDate` / `createdOnToDate` / `completedOnFromDate` / `completedOnToDate` request filters. **Read-only, no rule 2 exposure, no ask to Liberty.** **Build cautions recorded with it:** skip `maintenanceRequestFrom: "Entrata PaaS"` (7 of 785, turns not resident issues); collapse parent/child on `parentMaintenanceRequestId` (312 of 785 are children) before counting; do **not** segment on `maintenancePriority` (780 of 785 are `Medium`), use `maintenanceProblem` and `maintenanceLocation`; ⚠️ **`createdOn` is SPEC, so verify it comes back populated before building on it, because if it is empty the metric needs a different clock and the design changes**; and consider reporting **in-hours versus out-of-hours separately**, since office hours are M-F 9:00-5:00 with no weekend coverage and ~76% of the week has no staff. **Left open on purpose:** the target or threshold is unset (the 24hr / 4hr number is a **FIRST CONTACT** target, parked at Phase 2, **not** a resolution target), and whether the metric joins the six-KPI hero scoreboard is undecided. 📋 NPI markets under **SEC Rule 506(c)**, so a real operating metric in public marketing is permitted; standard characterization rules apply to forward-looking framing. §3.1, §4.2, open questions Tier 3 #19 / #20. **No writes are authorised by this.** |
| 2026-08-10 (decisions) | 💬 **BRENT'S DECISION: the resident follow-up is an AI-generated personal message about their specific issue.** Verbatim: *"an AI generated personal message about their specific issue, 'Did your back door get fixed?'"* **It is NOT the survey that was cut.** `lead-comms-spec.html` §4 cut the automated post-service satisfaction survey as impersonal and unsubscribe-prone, and **that cut STAYS cut**; this is the **lighter personal check-in**, and it is the thing that **survives the objection that killed the survey**, because it is specific, personal, and from the ownership number rather than a form. **The posture question is resolved: it does not conflict with "watch the scoreboard, not the plays," because the message goes to the RESIDENT, not to Liberty.** Nothing per-ticket is sent to, demanded of, or logged against Liberty. **Per-ticket outbound to a resident and per-ticket policing of a vendor are different acts.** **What it needs, all readable today with no new permission:** `maintenanceProblem`, `maintenanceLocation` and `maintenanceDescription` from `getWorkOrders`. **Mandatory filters, inherited from decisions already made:** skip `maintenanceRequestFrom: "Entrata PaaS"` (a system make-ready must never fire a resident message); collapse parent/child on `parentMaintenanceRequestId` so one turn sends one message; clear the `getMarketingPreferences` consent gate on **both** the LEAD and RESIDENT lens, with a missing preference meaning **do not send**; triage on `maintenanceProblem` / `maintenanceLocation`, never `maintenancePriority`. ⚠️ **NEW RISK, with a written rule: the message quotes the resident's own issue back to them, so a wrong or stale detail is worse than a generic message.** Pick-list values are terse and not always renderable into natural language ("Make Ready" being the obvious case, and it is exactly the system-generated record the first filter excludes). **The rule: the AI renders from the actual field values and never invents detail; if problem and location will not form a natural, confident sentence, fall back to a generic check-in or do not send.** **Constitution rule 8 (fair housing) applies**, as to any AI-generated resident-facing message. 📌 **Where it gets recorded: not in Entrata.** No lead-event write method exists in the 128-method spec, and `sendLeaseActivities` is not on the key, so the touch logs to **Foothold's own store**. ⚠️ **This strengthens the case for ask #19**, since a resident-level service touch is exactly what that method would carry. ⚠️ **[CORRECTED 2026-08-10, same day, factual correction only. "No lead-event write method exists" is WRONG and is withdrawn: `updateLeads` r2 and `sendLeads` r1 both append lead events and both are on the key (§1.4). THE DECISION AND THE DESTINATION ARE UNAFFECTED, because this message goes to a RESIDENT, not a lead, and the lease-level half of the reasoning (`sendLeaseActivities` is the only resident-record appender and is not on the key) stands on its own. Only the stated rationale was partly wrong. Row kept verbatim for the trail.]** 🚫 **Scope: NPI-owned properties once Foothold is live on them. NOT Owyhee Park**, which is Liberty's building with Liberty's residents; **reading their tickets does not licence messaging their residents.** **Still open:** the completion trigger (poll versus the `workorder.status.update` webhook, and which `maintenanceStatus` values count), the timing after completion, and 🆕 **whether this message and the review ask are one touch or two**, which nothing in the corpus had addressed. §2.5, §3.2, §4.2, asks #19, open questions Tier 3 #7 (closed) / #21 / #22 (new). **No writes are authorised by this.** |
| 2026-08-10 (correction) | ⚠️ **CORRECTION, and it is a big one: "there is no lead-event write method anywhere in the 128-method spec" was FALSE and is withdrawn everywhere.** **Where it came from:** `reference/entrata-touchpoints.md` §6.4, written 2026-08-05, which is the doc this corpus designates as the tie-breaker. **How far it got before it was caught: six files.** `entrata-touchpoints.md` §6.4, this document (§1.4 item 4, §2.6/asks #19, §3.2, the header summary, two 2026-08-05 provenance rows), `system-design.html` (§ the resident-follow-up callout and the decisions log), `lead-comms-spec.html` §4, `design-map.html`, and `CLAUDE.md`. **How it was disproved: direct inspection of the vendored spec on 2026-08-10.** `reference/entrata-openapi-2026-07-28.yaml`, `updateLeads-r2-request-schema` runs lines **11672-12023** and contains an **`events` array at 11923** with `eventId` (11931), `typeId` (11935, "Event type ID"), `date` (11943, "Event date and time (YYYY-MM-DDTHH:MM:SS)"), `type` (11947, enum at 11951: `CallFromProspect`, `CallToProspect`, `Appointment`, `Tour`, `EmailToProspect`, `EmailFromProspect`), `callData` (11958), and a free-text **`comments`** field (**11988**, "Event comments"). `sendLeads-r1-request-schema` carries an `events` array too (**10604**). **Both methods are permissioned on the key.** ✅ **Supporting evidence, recorded as evidence and NOT as a recommendation to use it:** `sendMitsLeads` r1's `EventType` enum includes `Note`, `IncomingText` and `OutgoingText` (**11202-11204**), so the vocabulary demonstrably exists in Entrata's model. ⚠️ **The real limitation, stated precisely, is much narrower than the claim it replaces: the r2 `type` enum has no native text, chat, or note value**, so logging a text or a chat means passing a **numeric `typeId`** resolved from `getLeadPickLists` (155 event types), which is **unverified and cannot be verified without issuing a write**. That is **Tier 3 #8, now live again rather than moot.** 🚫 **NOTHING HERE AUTHORISES ANYTHING. Appending a lead event is a WRITE and Constitution rule 2 governs it completely, under the same three conditions.** This is a correction about what the API *can* do, not about what we are *allowed* to do. **Two decisions were built partly on the false claim and both were corrected in place rather than quietly patched:** the **2026-08-10 resident-follow-up** decision (its lease-level half survives, `sendLeaseActivities` really is the only appender for a RESIDENT record and really is not on the key, so the decision and its destination are unaffected and only the rationale was partly wrong) and the **`sendLeaseActivities` ask #19** (kept as written, since it is for resident-level touches, with the "and nothing exists on the lead side either" part of its reasoning withdrawn). **Standing lesson: the tie-breaker doc was wrong once. Check the spec, not the summary.** |
| 2026-08-13 (decisions) | 🔒 **ALL TWENTY OPEN DECISIONS ANSWERED. THE DESIGN IS LOCKED.** Brent answered `DECISIONS.md` #1-20 in session on 2026-08-13; the answers live on that file's `→ ANSWER:` lines and are filed through this document as **DECIDED 2026-08-13 (Brent, DECISIONS.md session)** entries. Where each landed here: **#1** Liberty's surfaces are Entrata itself plus a five-line weekly digest, **no standalone Foothold page** (§1.4, §2.6, §4.1, Tier 1 #1); **#2** the first signature names `sendLeads` + `updateLeads` at go-live, work orders and lease activities on a second signature, and lead-event logging rides inside the first (§4.4); **#3** the guest-card trigger is AI judgment behind the hard (phone OR email) AND first-name gate (§1.2, Tier 2 #6); **#4** FlightDeck is internal-only today, built toward self-serve software (§3.4, §3.5, Tier 1 #2); **#5** lead-event logging builds against the numeric `typeId`, transcript and channel in `comments`, enum-map fallback, with the `typeId` verification itself still open and unverifiable without a write (§1.4, Tier 3 #8); **#6** the Welcome Profile ships as a Handwrytten welcome packet, the Thanksgiving card is the no-DOB fallback, and the digest flags missing DOBs (§2.2); **#7** work-order follow-up: poll, qualifying statuses in config, next business morning, plus an AI-judgment gate on which tickets qualify (§3.1, §3.2, Tier 3 #21); **#8** follow-up and review ask are TWO messages, event-fired, never reply-fired (§2.5, §3.2, Tier 3 #22); **#9** after-hours is hybrid, and an emergency gets the phone number handed over immediately, never a queued callback (§1.5, Tier 3 #12); **#10** tours: propose for agent-guided, commit for self-guided (§1.6, Tier 3 #9); **#11** consent: live check load-bearing plus nightly hygiene sync, missing preference means do-not-send (§1.8, §4.1, Tier 3 #10); **#12** the gift budget is ~$75/door/year (Module 2 preamble); **#13** the maintenance KPI gets NO target and goes ON the scoreboard, with no SLA negotiation with Liberty unless the data says so (§3.1, Tier 3 #20); **#14** "watch the scoreboard, not the plays" is PROMOTED into the Constitution (§3.2, Tier 3 #19, "Deliberately NOT asked"); **#15** the manual availability contingency is SPECCED not built, trigger: Aspen not on the key two weeks before go-live (Tier 2 #5); **#16** webhooks: provision now, bundled into the Aspen/Summerset onboarding, observe read-only at Owyhee, the 12-minute poll stays (§4.6, asks #4, Tier 3 #13; ⚠️ the receiver-placement conflict with the Cloudflare-Worker plan is flagged in §4.6 for build-spec); **#17** skip the Entrata testimonial object, Google + Apartments.com only (§2.5, Tier 3 #11); **#18** the Aspen/Summerset email is OVERTAKEN BY EVENTS, Liberty is already onboarding both (asks #7, Tier 2 #5); **#19** the `sendWorkOrders` + `sendLeaseActivities` email is TABLED, not sent (asks #18/#19, §2.6, §4.3); **#20** the Foothold lead-source ask bundles into that email whenever it goes, and the GHL-to-`getLeads` reconciler gets built regardless (asks #6, §1.2). ⚠️ **Locked is not buildable: the nine build-spec areas get written next, before any code.** 🚫 **No writes were made, none are authorized, and Constitution rule 2 is unchanged: nothing in the twenty answers authorises a write today.** |

**Related:** [`system-design.html`](../system-design.html) (Constitution, Entrata protocol, flows,
build order) · [`lead-comms-spec.html`](../lead-comms-spec.html) (comms behavior, touch map, edge
cases) · [`reference/entrata-api-context.md`](entrata-api-context.md) ·
[`reference/entrata-method-index.md`](entrata-method-index.md) ·
[`reference/entrata-webhooks-2026-03-04.md`](entrata-webhooks-2026-03-04.md) · Drive
`Marketing/30_Foothold/CONTEXT.md`.

**When intent changes, edit the INTENT block and say who changed it and when. When the API surface
changes, edit REALITY and add a provenance row. When a GAP gets decided, move the decision into
`system-design.html` and leave a pointer here.**
