🔄AppFolio Settings Appfolio Connections Platform
🎓 Getting Started: HTTP Basic Authentication
  1. Get Your Developer ID. AppFolio → your account name → Admin → Developer ID (Developer Details card). In this account the Developer ID is also your Client ID.
  2. Get Your Basic Auth Credentials. Beneath API Credentials, select Basic Auth, then Generate a Client Secret on that row. Store it somewhere safe — it's shown only once.
  3. Create Your Encoded String. In Mac Terminal run echo -n "{ClientID}:{ClientSecret}" | base64 and copy the result.
Replace in the request command:
  • {YOUR-DEVELOPER-ID} — your Developer ID (step 1)
  • {YOUR-ENCODED-STRING} — the Base64 value (step 3)
  • {resource} — the desired resource (for example, properties, tenants, or charges)
Note: You'll need separate credentials for each customer database when using Basic Auth.
🔑 Database API Credentials (v0)
🔐 Two pairs, two jobs: the Client ID + Secret build the Encoded String; then the Developer ID + Encoded String are what every request sends (X-AppFolio-Developer-ID header + Authorization: Basic). Developer ID and Client ID are different values.
AppFolio → account name → Admin → Developer ID card. Sent as X-AppFolio-Developer-ID.
Basic Auth — builds the Encoded String
From AppFolio → API Credentials → Basic Auth row (Client ID) + Generate a Client Secret.
= base64(ClientID:ClientSecret). Generate it with ⚙️ above, or in Terminal: echo -n "ClientID:ClientSecret" | base64 | tr -d '\n'
— Not tested
Test against
📄 API Docs ↗
ℹ️ Stored in Supabase (app_settings → key appfolio_dev_ctown). Used by Post Reports to create records in AppFolio (bills, vendors, etc.) via the appfolio-proxy Edge Function.
📡 How to Make an API Request
curl -g --request GET 'https://api.appfolio.com/api/v0/{resource}/' \
--header 'X-AppFolio-Developer-ID: {YOUR-DEVELOPER-ID}' \
--header 'Authorization: Basic {YOUR-ENCODED-STRING}'
A 200 means your credentials work. 401 = bad credentials. 403/404 = auth is fine but that resource is restricted (still proves the connection).
🎓 Nexus is a SEPARATE AppFolio account (nexusplus)
These v0 Database-API credentials are different from C-Town's — generate a fresh set inside the nexusplus account:
  1. Developer ID. nexusplus.appfolio.com → account name → Admin → Developer ID card.
  2. Basic Auth Client Secret. Under API Credentials → Basic Auth row → Generate a Client Secret (shown once — save it).
  3. Encoded String. ⚙️ Generate below, or Terminal: echo -n "{ClientID}:{ClientSecret}" | base64
🔑 Database API Credentials (v0) — Nexus
🔐 Two pairs, two jobs: the Client ID + Secret build the Encoded String; then the Developer ID + Encoded String are what every request sends (X-AppFolio-Developer-ID header + Authorization: Basic).
AppFolio (nexusplus) → account name → Admin → Developer ID card. Sent as X-AppFolio-Developer-ID.
Basic Auth — builds the Encoded String
From nexusplus → API Credentials → Basic Auth row (Client ID) + Generate a Client Secret.
= base64(ClientID:ClientSecret). Generate it with ⚙️ above, or in Terminal: echo -n "ClientID:ClientSecret" | base64 | tr -d '\n'
— Not tested
Test against
📄 API Docs ↗
ℹ️ Stored in Supabase (app_settings → key appfolio_dev_nexus). Used by Post Reports → Construction Bills (portfolio = Nexus) to create records in the nexusplus AppFolio account.
📥 Your Webhook URL
The appfolio-webhook Edge Function. Must be deployed with Verify JWT off (--no-verify-jwt) so AppFolio can reach it.
🔐 Every delivery is signed (X-JWS-Signature) and verified against AppFolio's public keys before it's trusted. Unverified events are still logged but flagged and never acted on. During setup, leave WEBHOOK_REQUIRE_VALID unset so you can confirm delivery, then set it to true.
⚙️ Setup steps
  1. Run appfolio-webhooks-migration.sql in Supabase.
  2. Deploy the function: supabase functions deploy appfolio-webhook --no-verify-jwt
  3. In AppFolio → Admin → Webhooks, add the URL above and name it.
  4. Under Topic Subscriptions, check the topics you want (below), then Send Test Event.
  5. The test (and all future events) appear in the log at the bottom of this page.
📚 Available Topics
TopicKeyEvents
🔔 Received Events
ReceivedTopicEventResourceSignature
📭
Loading events…
🔑 Credentials
— Not tested
📄 API Docs ↗
ℹ️ Credentials are stored securely in Supabase (app_settings). They are used by Manage Listings, Budget Dashboard, Classic Collections, and Client Orbit.
🔑 Credentials
— Not tested
📄 API Docs ↗
ℹ️ Credentials are stored securely in Supabase (app_settings). They are used by Manage Listings, Budget Dashboard, Classic Collections, and Client Orbit.
🔑 SmartMove Connection
— Not tested
📄 SmartMove ↗
ℹ️ Stored securely in Supabase (app_settings → key smartmove, admin/manager only) and read by the applicant-screening-submit Edge Function. The applicant enters their SSN on TransUnion's own site — it is never collected or stored here.
📋 Report Configurations
Portfolio Report Name JSON Key Status Last Synced Actions
📭
Loading configurations…
🔄 Report Data Cache
Portfolio Report Name Last Synced Rows Status Action
📭
Loading sync status…
💡 Synced data is stored in appfolio_report_data. Other tools (Classic Collections, Client Orbit, Budget Dashboard) read from this table — syncing here keeps all tools up to date without each tool hitting the AppFolio API directly.
🔧 Maintenance — repair work-order descriptions
Older work orders show (no description) because they were synced before AppFolio exposed the job description. This re-fills the issue text for those rows from the cached work_order report (job_description → service_request_description) — matched on WO#. No AppFolio call; safe to re-run.
⬆️ Upload a CSV report
⚖️ Totals comparison
Report Portfolio Manual CSV total Synced total Difference Status Action
🧮
Loading…
💡 The manual side is whatever CSV you upload here (stored in appfolio_manual_uploads). The synced side is the live pull in appfolio_report_data. A row is ✓ Match when the two totals are within $1.00 or 0.1%; otherwise ✕ Mismatch. Both sides sum the same column — re-upload after each sync to re-check.

🔎 Reconciliation — Sync Health

Proves every AppFolio report and reference table actually landed in WilCodex — fresh, complete, and internally consistent. Read-only.

Running checks…
🕒 Freshness & population — is each mirror table fresh and non-empty?
Mirror tableSourceRowsLast syncedStatus
Loading…
📦 Fan-out completeness — did every row the report pulled reach its dedicated table? (catches silent drops)
ReportPortfolioReport pulledIn tableStatus
Loading…
🔗 Consistency checks — does the data reconcile across tables?
CheckResultStatus
Loading…
ℹ️ How to read this. Green = healthy. Amber = worth a look (stale, or a count gap that may be legitimate). Red = the sync broke or dropped data — check Sync Status for the failing report, then re-run it. Freshness SLA: report tables are expected within ~12h (cron runs 6a/1p/6p ET); reference tables within ~30h.
🔁 The Data Pipeline
1
AppFolio API
Reports run live against C-Town & Nexus
→
2
Get Settings
Each report's json_key + request body
→
3
Sync
Pulls rows on a schedule / on demand
→
4
Cache Tables
appfolio_* tables in Supabase
→
5
Collections KPIs
Read from cache — never the API directly
📦 What each report powers
Receivables Activity
receivables_activity → appfolio_receivables_activity
Fields: receipt_date, receipt_amount (positive receipts and negative NSF reversals — summed net), property_name, portfolio
MTD Collected Collection Rate (numerator) Daily Collections calendar Forecast — actuals Per-building collected
Rent Roll
rent_roll → appfolio_rent_roll
Fields: rent, market_rent / computed_market_rent, status, tenant, property_id
Max Rents Collection Rate (denominator) Occupied / total units Market rent total
Delinquency
delinquency → appfolio_delinquency (+ raw in appfolio_report_data)
Fields: amount_receivable, 00_to30, 30_plus, 90_plus, rent
Delinquent Balance 90+ Days Critical Aging buckets (0-30 / 30+ / 90+) Tenants behind count
General Ledger
general_ledger → appfolio_general_ledger
Filter: account_name ~ "Prepaid Rent" (4200/4300), debit > 0, post_date in month
Prepaid Rent applied Forecast — prepay baseline
Tenant Unpaid Charges
tenant_unpaid_charges_summary → appfolio_tenant_unpaid_charges (via sync_tenant_unpaid_charges)
Fields: one row per unpaid charge — occupancy_id, charge type, amount
Unpaid charges breakdown Tenant ledger detail
Tenant Directory
tenant_directory → appfolio_tenant_directory
Fields: occupancy_id, move_in, portfolio, contact info
Tenant matching New move-ins (forecast)
✅ KPI → Source reference (QC checklist)
If a KPI looks wrong, re-sync the report in its row first, then check the field.
Collections KPIAppFolio ReportCache TableHow it's computed
MTD CollectedReceivables Activityappfolio_receivables_activityΣ receipt_amount in current month — positive receipts netted against negative NSF reversals (a bounced payment nets to $0)
Collection RateReceivables + Rent Rollappfolio_receivables_activity ÷ appfolio_rent_rollMTD Collected ÷ Max Rents
Max RentsRent Rollappfolio_rent_rollΣ rent across occupied units
Delinquent BalanceDelinquencyappfolio_delinquencyΣ amount_receivable
90+ Days CriticalDelinquencyappfolio_delinquencyΣ 90_plus bucket
Aging bucketsDelinquencyappfolio_delinquency00_to30 / 30_plus / 90_plus columns
Prepaid Rent appliedGeneral Ledgerappfolio_general_ledgerΣ debit where account ~ "Prepaid Rent", current month
Daily collectionsReceivables Activityappfolio_receivables_activityreceipt_amount grouped by receipt_date
Forecast — actualsReceivables + collections_activityappfolio_receivables_activity + collections_activityPortfolio actuals from AppFolio, agent actuals from in-app activity
Unpaid charges breakdownTenant Unpaid Chargesappfolio_tenant_unpaid_chargesOne row per unpaid charge, joined by occupancy_id
Tenant matching / move-inTenant Directoryappfolio_tenant_directoryoccupancy_id & move_in lookup
⚠️ QC notes. Collections always reads the cache tables, never AppFolio live — so a stale KPI almost always means a stale sync. Check Sync Status first. general_ledger, tenant_directory and tenant_unpaid_charges_summary are not in the default report seed — they sync through their own configs/RPCs, so confirm they're active under Get Settings.
🗄️ History preservation (month rollover). Receivables is a time-series — previous months stay saved. The report pulls a rolling window ({prev_month_start} → {month_end} = last month + this month), and the sync deletes/re-inserts only that window, never older months. So when the month rolls over, prior-month KPIs keep working. Snapshot reports (Rent Roll, Delinquency, Aged Payables) are point-in-time by design — they fully replace on each sync and show "now," not history.
💧 NSF (bounced payments). AppFolio records a bounced payment as two lines: the original positive receipt + a negative reversal. Both are now synced and netted in every collections total, so a bounced payment no longer inflates "collected" (this previously made some portfolios — e.g. Brick Haven — look better than they were). The reversal lands in the month it bounced. To audit NSF volume: in appfolio_receivables_activity, sum rows where receipt_amount < 0 grouped by portfolio; cross-check against the nsf count column on appfolio_delinquency.
⚡ These actions use the Developer Space credentials (Developer ID + Basic Auth), not the Reports credentials. Set those up first under Developer Space → C-Town.
📋 Post Actions
Portfolio Action Name Endpoint Status Last Submitted Actions
📭
Loading post actions…
🔄 Submission History
When Action Endpoint Status Result
📭
Loading submission history…
💡 Each submission is logged to appfolio_post_log with the request, the HTTP status, and AppFolio's response (including the new record's id on success).
🛡️ PREPARE mode. Nothing posts automatically. Building only stages payloads for review. The model: 1 vendor = 1 invoice = 1 transaction, many lines each allocated to Property / Unit / GL. Material lines → GL 6100, labor lines → GL 6110. Company-card / already-paid → Journal Entry; net-terms invoice → open AP Bill. Uses Developer Space creds.
📑 Staged Bills
Vendor Invoice Type Property Lines Total Status Actions
📭
Nothing staged yet — hit Build Staged Bills.
🔗 Crosswalks — Vendors · GL · Properties · Cards  (click to expand — map each card to its account here)
Vendor map — construction vendor → AppFolio VendorId
Construction vendorAppFolio VendorIdAppFolio nameTxn typeMgmt as payee
No vendor map yet — hit Pull Vendors.
GL map — construction GL text → AppFolio GlAccountId (edit to override)
Construction GLCodeAppFolio GlAccountId (UUID)AppFolio name
No GL map yet — hit Pull GL Accounts.
Property map — construction building → AppFolio v0 property UUID (edit to override)
BuildingAppFolio PropertyId (UUID)AppFolio name
No property map yet — hit Pull Properties.
Cards — each company card → AppFolio cash/GL account (card-paid material bills post as PAID against it)
CardKeyAppFolio cash / GL account
No cards yet — hit 💳 Cash Accounts & Cards.