← All reports
Process map — acquisition to exit
How a person becomes a lead, an applicant, a resident, and eventually a former resident — and every system, role, and trigger in between.
How to read this page: Every arrow is a real handoff in the platform. Mermaid charts show data flow + actors. Tables under each chart describe what kicks off each step + which roles are accountable. Drill-throughs go to the underlying ADRs and dashboards.
1. Lead source map
Every funnel-top channel funnels into the same Postgres lead_inbound table and emits a crm_sync_event mirrored to Dataverse. Ad-network pixels (Meta CAPI, TikTok, Google Ads, Plausible) fan out client-side + server-side, deduped by event_id.
flowchart LR
subgraph PaidChannels["Paid channels"]
fb[Facebook Lead Ad]
google[Google Ads search]
tiktok[TikTok Lead Ad]
end
subgraph OrganicChannels["Organic + offline"]
driveby[Drive-by sign]
referral[Resident referral]
walkin[Walk-in tablet]
phone[Inbound phone]
end
subgraph LandingForms["crosbyheights.com / willislakes.com"]
inq[/inquire form/]
tour[/tour form/]
call[/schedule-call form/]
apply[Apply Now]
end
fb-->|FB Leadgen webhook| api
google-->|landing visit| LandingForms
tiktok-->|landing visit| LandingForms
driveby-->LandingForms
referral-->LandingForms
walkin-->|tablet POST| api
phone-->|operator logs| api
inq-->|POST /api/intake/inquire| api
tour-->|POST /api/intake/tour| api
call-->|POST /api/intake/schedule-call| api
apply-->|POST /api/apply/inquire| api[(portal-api)]
api-->|insert| li[(lead_inbound)]
api-->|emit| sync[(crm_sync_event)]
sync-->|drain worker| dv[(Dataverse · lead)]
api-.->|fan out| pixels[Meta CAPI / TikTok / Google / Plausible]
| Channel | Path | What's required to wire |
| Landing inquire/tour/call | CF Function → portal-api intake | Live — shipped 2026-05-15 |
| Apply-form inquire | portal-api /api/apply/inquire | Live |
| Facebook Leadgen | FB webhook → portal-api | Pending Meta App registration + page subscription (task #175) |
| Google Lead Form | Google webhook → portal-api | Not started — needs Google Ads campaign with lead form |
| TikTok Lead Ad | TikTok Pixel API → portal-api | Not started |
| Walk-in tablet | POST /api/intake/walkin | Endpoint stubbed; UI is task #67 |
| Phone call log | POST /api/intake/phone-log | Endpoint stubbed; UI is task #66 |
2. Applicant flow (ADR-0030 v3)
Mobile-first form, OTP-gated, with Plaid IDV up front for both primary and co-applicant before the application fee — so we screen out bad data before paying $25/applicant for Kredifi background checks.
sequenceDiagram
participant App as Applicant
participant Form as apply.<site>.com
participant API as portal-api
participant Plaid
participant ACS as ACS SMS
participant AN as AuthNet
participant Kr as Kredifi
App->>Form: visits apply
Form->>API: POST /apply/signup (email, phone)
API->>ACS: send OTP
ACS-->>App: SMS code
App->>Form: enters code
Form->>API: POST /apply/otp/verify
API-->>Form: applicant_id + session
Form->>API: POST /plaid/link-token (product=idv)
API->>Plaid: create IDV session
Plaid-->>API: link_token
API-->>Form: link_token
Form->>App: Plaid Link iframe — gov-ID + selfie
Form->>API: POST /plaid/idv/result
Note over Form,API: ↓ if co-applicant
Form->>API: POST /apply/coapp/invite (email, phone)
API->>ACS: SMS deep link to co-app
Note over App: Co-app does the same flow on their device
Form->>API: POST /payment/charge (Accept.js opaque_data)
API->>AN: createTransaction
AN-->>API: trans_id
API->>API: emit application.fee_paid sync event
API->>Plaid: trigger income link_token (both applicants)
API->>Kr: createOrder (background check, both)
Kr-->>API: order_id + status
API->>ACS: SMS decision to primary
| Step | Trigger | Records written |
| Signup | OTP verified | applicant_credential + application + application.started sync |
| Primary IDV | Plaid Link onSuccess | plaid_session(idv, success) + application.idv_completed sync |
| Co-applicant invited | "Yes I have a co-app" | applicant_invitation + application.coapplicant_invited sync + ACS SMS |
| Co-app IDV | Co-app completes via deep link | plaid_session + application.idv_completed sync |
| Fee paid | AuthNet capture succeeds | payment + application.status='paid' + application.fee_paid sync |
| Income verified | Plaid income complete (each applicant) | plaid_session(income, success) + application.income_verified sync |
| Screening ordered | Both IDV+income done | kredifi_screening + application.screening_ordered sync |
| Screening result | Kredifi poll or webhook | kredifi_screening.recommendation + application.screening_complete sync |
3. Payment + screening (autofire chain)
flowchart TB
pay[application.fee_paid] -->|cron 60s| adv[apply_state_advancer]
adv -->|both IDV success?| inc[fire Plaid income link tokens]
inc -->|both income success?| krfan[fire Kredifi for both applicants]
krfan -->|poll status / webhook| dec[decision rules]
dec -->|score >= threshold| ok[approve]
dec -->|score < threshold| cond[conditional / manual review]
dec -->|disqualified| deny[deny → adverse action letter]
ok --> docs[lease envelope via Documenso]
cond --> pm[PM review queue]
deny --> fcra[FCRA adverse action notice + 30d dispute hold]
4. Move-in prep
flowchart LR
signed[lease signed in Documenso] -->|webhook| portal[portal-api]
portal -->|emit| lc[lease.signed sync event]
portal --> inspect[inspection.scheduled]
inspect --> tech[Tech does inspection]
tech -->|inspection cleared| ppsk[PPSK code generation]
tech -->|inspection has holds| holds[blockers queue · PM review]
ppsk -->|Assa Abloy API| gate[Gate code programmed]
gate --> flip[Portal mode flip → resident]
flip --> welcome[Welcome email + first invoice]
5. Resident steady state
flowchart LR
cron[weekly cron] --> inv[invoice.created · rent + utilities]
inv -->|AuthNet ARB or ACH| capture[payment.captured]
inv -->|NSF / decline| failed[payment.failed → arrears flag]
resident[Resident submits ticket] --> maint[maintenance.requested]
maint -->|PM assigns| tech[Technician]
tech -->|completes| done[maintenance.completed]
cron2[lease renewal cron · 60/30/14d before end] --> offer[lease.renewal_offered]
offer -->|sign| ren[lease.renewed → chained lease row]
6. Exit + SODA + refund
flowchart LR
ntv[Resident NTV] -->|portal/PM| ntvd[lease.ntv_given]
ntvd --> mout[move-out inspection scheduled]
mout -->|tech submits| moutc[inspection.move_out_completed]
moutc --> drft[soda.drafted — auto from move-out form]
drft -->|PM reviews| fin[soda.finalized — 7-day rule]
fin --> dmg[damages.invoiced if applicable]
fin --> ref[refund.issued — AuthNet first, check fallback]
ref -->|unclaimed 90d| esc[refund.escheated]
fin --> mo[resident.moved_out → lease.closed]
7. Role responsibilities at a glance
| Step | Owner | What they do |
| Application review | PM | Decide approve/conditional/deny on borderline Kredifi scores via /funnel |
| Tour scheduling | Applicant + PM | Applicant self-books on landing; PM gets .ics calendar invite to property mailbox |
| Send contract | PM | Click "Send lease" in /funnel; portal fires Documenso envelope |
| Move-in inspection | Tech | Walks pad, photos, signs off via inspection app; clears or holds |
| PPSK + gate code | System | Auto-fires on inspection cleared; SMS code to resident |
| Rent capture | System | Weekly AuthNet ARB pulls + Plaid balance pre-flight |
| Bank rec | Accountant | BC reconciliation, Zelle match, salesreceipt verification |
| SODA finalization | PM | Within 7 days of move-out per ADR-SODA |
| Refund disbursement | System → Accountant | AuthNet refund first; if fails, check via accountant; escheatment timer |
8. System of record matrix
| What lives where | Postgres (portal) | Dataverse (CRM) | BC (Business Central) | R2 | Vault |
| Applicants + state | SoR | mirror | — | — | — |
| Leads (PM read surface) | — | SoR for read | — | — | — |
| Invoices + payments | SoR | mirror | book of record | — | — |
| Leases | SoR | mirror | — | signed PDFs | — |
| Inspection photos / docs | metadata only | — | — | SoR | — |
| Secrets / creds | — | — | — | — | SoR (gr-portal-kv) |
| Bank statements | — | — | SoR | archive | — |
9. External integrations
| Service | What it does for us | Status |
| Plaid | Identity verification + income verification + balance pre-flight before ACH | Wired (sandbox) · prod creds in vault |
| Kredifi | FCRA background screening (credit + criminal + eviction) | Stub built · UAT creds pending (task #185) |
| Authorize.Net | Card + ACH (eCheck.Net) charges; ARB for autopay | Login + transaction keys in vault · signature keys pending (task #190) |
| Documenso | E-signature for leases, NTV, adverse-action letters, renewals | Not yet wired |
| PPSK | Property-wide WiFi passkey + gate access | Not yet wired · GH repo gated-rentals/ppsk |
| Assa Abloy | Gate hardware code programming | Not yet wired · manual today |
| ACS SMS | OTP + transactional SMS · per-tenant toll-free | Crosby +1 (866) 366-5403 · Willis +1 (866) 366-5727 · toll-free verification pending |
| ACS Email | Tour .ics + decision notices + transactional email | Connection-string slot exists; wrapper shipped 2026-05-15 |
| Microsoft Bookings | (Replaced by /api/intake/tour with .ics) | Deprecated 2026-05-15 |
10. Failure paths + escalations
| What goes wrong | How we catch it | What happens next |
| Application denied | Kredifi score below threshold | FCRA adverse-action letter via Documenso · 30-day dispute hold on file · automatic destroy of report at retention end |
| Card declined | AuthNet sync response code != 1 | UI shows reason; applicant re-tries with different card; no state change |
| ACH returned (NSF) | AuthNet eCheck webhook · or daily statement scrape | arrears flag · late notice cron · returned-payment fee from rate sheet |
| Sync response lost but charge succeeded | AuthNet webhook + reconciler cron | Idempotent: webhook fills in the missing payment row via authnet_webhook_event.notification_id |
| Move-out without SODA | Daily cron · finrep cross-check | 30/60/90d reminders to PM · escalation to accounting |
| Refund unclaimed | refund.issued timer | 90d → escheatment workflow to Texas Comptroller |
| Webhook signature mismatch | HMAC verification fails | Logged as dropped_unverified in authnet_webhook_event · alert on rate |
| CRM sync failure | crm_sync_event.status='failed' with last_error | Exponential backoff (2^attempts seconds capped at 1h, max 8) · PM dashboard shows stuck events |
| Plaid IDV fail | Plaid Link onSuccess with status='failed' | Applicant offered a second attempt; if 2nd fails, manual review queue for PM |
Sources of truth backing this page:
ADR-0021 (portal-first / intentional sync) ·
ADR-0028 (applicant state machine) ·
ADR-0029 (applicant flow v2) ·
ADR-0030 (applicant flow v3, IDV up front) ·
ADR-0031 (CRM sync event catalog).
Open docs/decisions/ in the platform repo for full text.
Generated 2026-05-15 · Updates when ADRs change · Mermaid renders via cdn.jsdelivr.net