409 Conflict |
Work order already closed |
Check
Optimize AppFolio API costs through efficient usage patterns.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Cost Tuning
Overview
AppFolio Stack API pricing is partner-agreement based, with costs scaling by API call volume per managed property. Property management portfolios generate high-frequency reads for tenant lookups, lease status checks, and maintenance requests. Each redundant API call erodes margin on per-unit revenue. Optimizing call patterns directly impacts operational profitability, especially for portfolios managing hundreds or thousands of units where even small per-call costs compound rapidly.
Cost Breakdown
| Component |
Cost Driver |
Optimization |
| Property/unit reads |
Per-call pricing on tenant and unit endpoints |
Cache with 10-15 min TTL; property data changes infrequently |
| Lease operations |
Bulk lease queries across entire portfolio |
Fetch all leases once, filter locally instead of per-unit calls |
| Maintenance requests |
Polling for new work orders |
Use webhooks to receive push notifications |
| Reporting exports |
Large payload downloads for financial reports |
Schedule off-peak, cache results for 24h |
| Vendor/owner lookups |
Repeated lookups for the same contacts |
Build a local lookup table, refresh daily |
API Call Reduction
class AppFolioCache {
private cache = new Map<string, { data: any; expiry: number }>();
get(key: string): any | null {
const entry = this.cache.get(key);
if (!entry || Date.now() > entry.expiry) return null;
return entry.data;
}
set(key: string, data: any, ttlMs = 600_000): void {
this.cache.set(key, { data, expiry: Date.now() + ttlMs });
}
async fetchWithCache(endpoint: string, ttlMs?: number): Promise<any> {
const cached = this.get(endpoint);
if (cached) return cached;
const response = await fetch(endpoint);
const data = await response.json();
this.set(endpoint, data, ttlMs);
return data;
}
}
Usage Monitoring
class AppFolioUsageMonitor {
private calls: Array<{ endpoint: string; timestamp: number }> = [];
private budgetLimit = 10_000; // daily call budget
record(endpoint: string): void {
this.calls.push({ endpoint, timestamp: Date.now() });
const todayCalls = this.getTodayCount();
if (todayCalls > this.budgetLimit * 0.8) {
console.warn(`AppFolio API budget 80% consumed: ${todayCalls}/${this.budgetLimit}`);
}
}
getTodayCount(): number {
const startOfDay = new Date().setHours(0, 0, 0, 0);
return this.calls.filter(c => c.timestamp > startOfDay).length;
}
}
Cost Optimization Checklist
- [ ] Cache property and unit data with 10-15 min TTL
- [ ] Replace polling loops wit
Collect AppFolio API debug evidence for support tickets.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Debug Bundle
Overview
This debug bundle collects diagnostic evidence from AppFolio property management API integrations
for support escalation and root cause analysis. It captures API connectivity against the
properties, tenants, and work orders endpoints, authentication status using client credential
pairs, recent error logs from integration pipelines, and SDK version information. The resulting
tarball gives support engineers everything they need to diagnose connectivity failures, auth
rejections, and data sync issues without requiring live access to your environment.
Prerequisites
curl, jq, tar installed
APPFOLIOCLIENTID and APPFOLIOCLIENTSECRET configured (basic auth pair)
APPFOLIOBASEURL set to your Stack API base (e.g., https://yourcompany.appfolio.com/api/v1)
Debug Collection Script
#!/bin/bash
set -euo pipefail
BUNDLE="debug-appfolio-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BUNDLE"
# Environment check
echo "=== Environment ===" > "$BUNDLE/environment.txt"
echo "Base URL: ${APPFOLIO_BASE_URL:-NOT SET}" >> "$BUNDLE/environment.txt"
echo "Client ID: ${APPFOLIO_CLIENT_ID:+SET (redacted)}" >> "$BUNDLE/environment.txt"
echo "Client Secret: ${APPFOLIO_CLIENT_SECRET:+SET (redacted)}" >> "$BUNDLE/environment.txt"
echo "Node: $(node -v 2>/dev/null || echo 'not installed')" >> "$BUNDLE/environment.txt"
echo "Timestamp: $(date -u)" >> "$BUNDLE/environment.txt"
# API connectivity — properties endpoint
echo "=== API Health ===" > "$BUNDLE/api-health.txt"
curl -sf -o "$BUNDLE/api-health.txt" -w "HTTP %{http_code} in %{time_total}s\n" \
-u "${APPFOLIO_CLIENT_ID}:${APPFOLIO_CLIENT_SECRET}" \
"${APPFOLIO_BASE_URL}/properties?per_page=1" 2>&1 || echo "UNREACHABLE" > "$BUNDLE/api-health.txt"
# Work orders endpoint probe
echo "=== Work Orders ===" > "$BUNDLE/work-orders.txt"
curl -sf -w "HTTP %{http_code}\n" \
-u "${APPFOLIO_CLIENT_ID}:${APPFOLIO_CLIENT_SECRET}" \
"${APPFOLIO_BASE_URL}/work_orders?per_page=1" >> "$BUNDLE/work-orders.txt" 2>&1 || echo "FAILED" >> "$BUNDLE/work-orders.txt"
# Tenant endpoint probe
echo "=== Tenants ===" > "$BUNDLE/tenants.txt"
curl -sf -w "HTTP %{http_code}\n" \
-u "${APPFOLIO_CLIENT_ID}:${APPFOLIO_CLIENT_SECRET}" \
"${APPFOLIO_BASE_URL}/tenants?per_page=1" >> "$BUNDLE/tenants.txt" 2>&1 || echo "FAILED
Deploy AppFolio integration service to cloud infrastructure.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Deploy Integration
Overview
Deploy a containerized AppFolio property management integration service with Docker. This skill covers building a production-ready image that connects to the AppFolio Stack API for managing properties, tenants, and work orders. Includes environment configuration for multi-property setups, health checks that verify API connectivity, and rolling update strategies for zero-downtime deployments across your property portfolio.
Docker Configuration
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
FROM node:20-slim
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
Environment Variables
export APPFOLIO_API_KEY="af_live_xxxxxxxxxxxx"
export APPFOLIO_BASE_URL="https://your-company.appfolio.com/api/v1"
export APPFOLIO_COMPANY_ID="your-company"
export LOG_LEVEL="info"
export PORT="3000"
export NODE_ENV="production"
Health Check Endpoint
import express from 'express';
const app = express();
app.get('/health', async (req, res) => {
try {
const response = await fetch(`${process.env.APPFOLIO_BASE_URL}/properties`, {
headers: { 'Authorization': `Bearer ${process.env.APPFOLIO_API_KEY}` },
});
if (!response.ok) throw new Error(`AppFolio API returned ${response.status}`);
res.json({ status: 'healthy', service: 'appfolio-integration', timestamp: new Date().toISOString() });
} catch (error) {
res.status(503).json({ status: 'unhealthy', error: (error as Error).message });
}
});
Deployment Steps
Step 1: Build
docker build -t appfolio-integration:latest .
Step 2: Run
docker run -d --name appfolio-integration \
-p 3000:3000 \
-e APPFOLIO_API_KEY -e APPFOLIO_BASE_URL -e APPFOLIO_COMPANY_ID \
appfolio-integration:latest
Step 3: Verify
curl -s http://localhost:3000/health | jq .
Step 4: Rolling Update
docker build -t appfolio-integration:v2 . && \
docker stop appfolio-integration && \
docker rm appfolio-integration && \
docker run -d --name appfolio-integration -p 3000:3000 \
-e APPFOLIO_API_KEY -e APPFOLIO_BASE_URL -e APPFOLIO_COMPANY_ID \
appfolio-integration:v2
Query AppFolio properties, units, and tenants via REST API.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Hello World
Overview
Get started with the AppFolio Property Manager API by authenticating with your client credentials and making your first API calls. This skill walks through connecting to the REST API, fetching a property listing, retrieving tenant details, and creating a basic work order — the essential operations for any AppFolio integration.
Prerequisites
- AppFolio Stack Partner account with API access
APPFOLIOCLIENTID and APPFOLIOCLIENTSECRET environment variables set
- Node.js 18+ and TypeScript
Instructions
Step 1: Configure the Client
const APPFOLIO_BASE = process.env.APPFOLIO_BASE_URL || "https://yourcompany.appfolio.com/api/v1";
async function appfolioFetch(path: string) {
const credentials = Buffer.from(
`${process.env.APPFOLIO_CLIENT_ID}:${process.env.APPFOLIO_CLIENT_SECRET}`
).toString("base64");
const res = await fetch(`${APPFOLIO_BASE}${path}`, {
headers: { Authorization: `Basic ${credentials}`, Accept: "application/json" },
});
if (!res.ok) throw new Error(`AppFolio ${res.status}: ${await res.text()}`);
return res.json();
}
Step 2: List Properties
const properties = await appfolioFetch("/properties?page_size=10");
console.log(`Found ${properties.length} properties`);
properties.forEach((p: any) => console.log(` ${p.id}: ${p.address_line1}, ${p.city}`));
Step 3: Get Tenant Details
const tenants = await appfolioFetch(`/tenants?property_id=${properties[0].id}`);
tenants.forEach((t: any) => console.log(` ${t.name} — Unit ${t.unit_number}`));
Step 4: Create a Work Order
const workOrder = await fetch(`${APPFOLIO_BASE}/work_orders`, {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${process.env.APPFOLIO_CLIENT_ID}:${process.env.APPFOLIO_CLIENT_SECRET}`).toString("base64")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
property_id: properties[0].id,
description: "Leaky faucet in kitchen",
priority: "normal",
}),
});
console.log("Work order created:", (await workOrder.json()).id);
Output
A successful run produces authenticated API responses: a list of properties with addresses, tenant details for the first property, and a new work order ID confirming write access.
Error Handling
| Error |
Cause |
Solution |
401 Unauthorized |
Invalid client_id or secret |
Verify credentials in environment variables |
403 Forbidden |
API scope
Configure AppFolio Stack API authentication with OAuth 2.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Install & Auth
Overview
Configure AppFolio Stack API authentication. AppFolio uses HTTP Basic Auth with a client ID and client secret, provided through their Stack partner program. No public npm SDK exists — use direct REST API calls.
Prerequisites
- AppFolio Stack partner account (appfolio.com/stack)
- Client ID and Client Secret from AppFolio
- Node.js 18+ or Python 3.10+
Instructions
Step 1: Obtain API Credentials
# AppFolio Stack API credentials come from the partner program
# 1. Apply at appfolio.com/stack/become-a-partner
# 2. Complete integration review
# 3. Receive client_id and client_secret
cat > .env << 'ENVFILE'
APPFOLIO_CLIENT_ID=your-client-id
APPFOLIO_CLIENT_SECRET=your-client-secret
APPFOLIO_BASE_URL=https://your-company.appfolio.com/api/v1
ENVFILE
chmod 600 .env
echo ".env" >> .gitignore
Step 2: Create API Client
// src/appfolio-client.ts
import axios, { AxiosInstance } from 'axios';
class AppFolioClient {
private api: AxiosInstance;
constructor() {
this.api = axios.create({
baseURL: process.env.APPFOLIO_BASE_URL,
auth: {
username: process.env.APPFOLIO_CLIENT_ID!,
password: process.env.APPFOLIO_CLIENT_SECRET!,
},
headers: { 'Content-Type': 'application/json' },
timeout: 30000,
});
}
async verifyConnection(): Promise<boolean> {
try {
const response = await this.api.get('/properties');
console.log(`Connected! Found ${response.data.length} properties`);
return true;
} catch (error: any) {
console.error(`Connection failed: ${error.response?.status} ${error.message}`);
return false;
}
}
get http(): AxiosInstance { return this.api; }
}
export { AppFolioClient };
Step 3: Verify Connection
# Quick curl test
curl -u "${APPFOLIO_CLIENT_ID}:${APPFOLIO_CLIENT_SECRET}" \
"${APPFOLIO_BASE_URL}/properties" | jq '.[0]'
API Endpoints
| Resource |
Endpoint |
Methods |
| Properties |
/api/v1/properties |
GET |
| Units |
/api/v1/units |
GET |
| Tenants |
/api/v1/tenants |
GET |
| Leases |
/api/v1/leases |
GET, POST |
| Bills |
/api/v1/bills |
GET, POST |
| Vendors |
/api/v1/vendors |
GET |
| Owners |
/api/v1/owners |
GET |
| Reports |
/api/v1/reports
Set up local development for AppFolio property management API integration.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Local Dev Loop
Overview
Local development workflow for AppFolio property management API integration. Provides a fast feedback loop with mock property, tenant, and lease endpoints so you can build and test integrations without consuming live API quota. Toggle between mock mode for rapid iteration and sandbox mode for pre-deployment validation against the real AppFolio Stack API.
Environment Setup
cp .env.example .env
# Set your credentials:
# APPFOLIO_API_KEY=af_live_xxxxxxxxxxxx
# APPFOLIO_BASE_URL=https://api.appfolio.com/api/v1
# MOCK_MODE=true
npm install express axios dotenv tsx typescript @types/node
npm install -D vitest supertest @types/express
Dev Server
// src/dev/server.ts
import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
const app = express();
app.use(express.json());
const MOCK = process.env.MOCK_MODE === "true";
if (!MOCK) {
app.use("/api/v1", createProxyMiddleware({
target: process.env.APPFOLIO_BASE_URL,
changeOrigin: true,
headers: { Authorization: `Bearer ${process.env.APPFOLIO_API_KEY}` },
}));
} else {
const { mountMockRoutes } = require("./mocks");
mountMockRoutes(app);
}
app.listen(3001, () => console.log(`AppFolio dev server on :3001 [mock=${MOCK}]`));
Mock Mode
// src/dev/mocks.ts — realistic property management responses
export function mountMockRoutes(app: any) {
app.get("/api/v1/properties", (_req: any, res: any) => res.json([
{ id: "prop_1", name: "Sunset Apartments", address: { street: "123 Sunset Blvd", city: "Los Angeles", state: "CA" }, property_type: "residential", unit_count: 24 },
{ id: "prop_2", name: "Downtown Office", address: { street: "456 Main St", city: "San Francisco", state: "CA" }, property_type: "commercial", unit_count: 8 },
]));
app.get("/api/v1/tenants", (_req: any, res: any) => res.json([
{ id: "t1", first_name: "Jane", last_name: "Smith", email: "jane@example.com", unit_id: "u1", lease_id: "l1" },
]));
app.get("/api/v1/leases", (_req: any, res: any) => res.json([
{ id: "l1", unit_id: "u1", start_date: "2025-01-01", end_date: "2026-01-01", rent_amount: 2500, status: "active" },
]));
app.post("/api/v1/work-orders", (req: any, res: any) => res.status(201).json({ id: "wo_1", ...req.body, status: "open" }));
}
Testing Workflow
npm run dev:mock & # Start mock server in background
npm run test
Optimize AppFolio API performance with caching and batch operations.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Performance Tuning
Overview
AppFolio's property management API handles bulk tenant queries, property portfolio pagination, and work order batch processing. Large portfolios with thousands of units generate heavy read traffic on listing endpoints. Optimizing cache lifetimes for slow-changing property data, batching work order updates, and pooling HTTP connections reduces API call volume by 60-80% and cuts dashboard load times from seconds to sub-second.
Caching Strategy
const cache = new Map<string, { data: any; expiry: number }>();
const TTL = { properties: 300_000, tenants: 120_000, units: 300_000, workOrders: 60_000 };
async function cached(key: string, ttlKey: keyof typeof TTL, fn: () => Promise<any>) {
const entry = cache.get(key);
if (entry && entry.expiry > Date.now()) return entry.data;
const data = await fn();
cache.set(key, { data, expiry: Date.now() + TTL[ttlKey] });
return data;
}
Batch Operations
async function batchWorkOrders(client: any, ids: string[], batchSize = 25) {
const results = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const res = await Promise.all(batch.map(id => client.http.get(`/work_orders/${id}`)));
results.push(...res.map(r => r.data));
if (i + batchSize < ids.length) await new Promise(r => setTimeout(r, 200));
}
return results;
}
Connection Pooling
import { Agent } from 'https';
const agent = new Agent({ keepAlive: true, maxSockets: 10, maxFreeSockets: 5, timeout: 30_000 });
// Pass to axios/fetch: { httpsAgent: agent }
Rate Limit Management
async function withRateLimit(fn: () => Promise<any>): Promise<any> {
const res = await fn();
const remaining = parseInt(res.headers['x-ratelimit-remaining'] || '100');
if (remaining < 5) {
const retryAfter = parseInt(res.headers['retry-after'] || '2') * 1000;
await new Promise(r => setTimeout(r, retryAfter));
}
return res;
}
Monitoring
const metrics = { apiCalls: 0, cacheHits: 0, errors: 0, totalLatency: 0 };
function track(startMs: number, hit: boolean, error?: boolean) {
metrics.apiCalls++; metrics.totalLatency += Date.now() - startMs;
if (hit) metrics.cacheHits++; if (error) metrics.errors++;
}
// Log: avg latency, cache hit rate, error rate per minute
Performance Checklist
- [ ] Cache property and unit listings with 5-min TTL
- [ ] Use incremental sync via last_modified timestamps
- [ ] Batch work order updates in groups of 25
- [ ] Enable HTTP keep-alive with connection pooling
- [ ] Parse rate limit head
Production readiness checklist for AppFolio integrations.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Production Checklist
Overview
AppFolio manages properties, tenants, leases, and work orders for real estate operations. A production integration handles sensitive tenant PII, financial transactions, and maintenance workflows. Failures here mean missed rent collections, unprocessed work orders, or tenant data exposure under CCPA. This checklist ensures your AppFolio API integration is resilient, compliant, and observable.
Authentication & Secrets
- [ ]
APPFOLIOAPIKEY stored in secrets manager (not environment files)
- [ ] Client ID and secret separated from application code
- [ ] Key rotation schedule documented (90-day recommended)
- [ ] Separate credentials for dev/staging/prod environments
- [ ] API credentials scoped to minimum required permissions
API Integration
- [ ] Production base URL configured (
https://api.appfolio.com/v1)
- [ ] Rate limit handling with exponential backoff
- [ ] Pagination implemented for property and tenant list endpoints
- [ ] Work order creation tested with all required fields
- [ ] Lease document upload validated for supported formats
- [ ] Webhook endpoints configured for tenant and payment events
- [ ] Idempotency keys used for payment and work order creation
Error Handling & Resilience
- [ ] Circuit breaker configured for AppFolio API outages
- [ ] Retry with backoff for 429/5xx responses
- [ ] Tenant PII handling verified CCPA/FCRA compliant
- [ ] Data validation on all API responses before storage
- [ ] Graceful degradation when property sync is unavailable
- [ ] Duplicate work order detection prevents re-creation on retry
Monitoring & Alerting
- [ ] API latency tracked per endpoint (properties, tenants, work orders)
- [ ] Error rate alerts set (threshold: >3% over 5 minutes)
- [ ] Failed payment sync triggers immediate P1 alert
- [ ] Work order creation failures reported within 5 minutes
- [ ] Daily reconciliation of synced property counts vs source
Validation Script
async function checkAppFolioReadiness(): Promise<void> {
const checks: { name: string; pass: boolean; detail: string }[] = [];
const baseUrl = process.env.APPFOLIO_BASE_URL || 'https://api.appfolio.com/v1';
// API connectivity
try {
const res = await fetch(`${baseUrl}/properties?limit=1`, {
headers: { Authorization: `Bearer ${process.env.APPFOLIO_API_KEY}` },
});
checks.push({ name: 'API Connectivity', pass: res.ok, detail: res.ok ? 'Connected' : `HTTP ${res.status}` });
} catch (e: any) { checks.push({ name: 'API Connectivity', pass: false, detail: e.message }); }
// Credentials present
checks.push({ name: 'API Key Set', p
Handle AppFolio API rate limits with throttling and backoff.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Rate Limits
Overview
AppFolio's Stack API enforces per-partner rate limits to protect shared property management infrastructure. High-volume operations like bulk tenant imports, rent-roll syncs, and work-order batch updates can quickly exhaust quotas. Property managers running nightly portfolio syncs across hundreds of units must throttle carefully, especially during month-end when lease renewals and payment processing spike concurrently.
Rate Limit Reference
| Endpoint |
Limit |
Window |
Scope |
| Properties list/get |
120 req |
1 minute |
Per partner key |
| Tenant create/update |
30 req |
1 minute |
Per partner key |
| Work orders |
60 req |
1 minute |
Per partner key |
| Bulk data export |
5 req |
1 hour |
Per partner key |
| Webhooks registration |
10 req |
1 minute |
Per partner key |
Rate Limiter Implementation
class AppFolioRateLimiter {
private tokens: number;
private lastRefill: number;
private readonly maxTokens: number;
private readonly refillRate: number; // tokens per ms
private queue: Array<{ resolve: () => void }> = [];
constructor(maxPerMinute: number) {
this.maxTokens = maxPerMinute;
this.tokens = maxPerMinute;
this.lastRefill = Date.now();
this.refillRate = maxPerMinute / 60_000;
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens >= 1) { this.tokens -= 1; return; }
return new Promise(resolve => this.queue.push({ resolve }));
}
private refill() {
const now = Date.now();
this.tokens = Math.min(this.maxTokens, this.tokens + (now - this.lastRefill) * this.refillRate);
this.lastRefill = now;
while (this.tokens >= 1 && this.queue.length) {
this.tokens -= 1;
this.queue.shift()!.resolve();
}
}
}
const limiter = new AppFolioRateLimiter(100);
Retry Strategy
async function appfolioRetry<T>(fn: () => Promise<Response>, maxRetries = 4): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
await limiter.acquire();
const res = await fn();
if (res.ok) return res.json();
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get("Retry-After") || "10", 10);
const delay = retryAfter * 1000 + Math.random() * 2000;
await new Promise(r => setTimeout(r, delay));
continue;
}
if (res.status >= 500 && attempt < maxRetries) {
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
continue;
}
throw new Error(`AppFolio API ${res.status}: ${await r
Reference architecture for AppFolio property management integration.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Reference Architecture
Overview
Production architecture for property management integrations with the AppFolio Stack API. Designed for multi-property portfolios requiring real-time vacancy tracking, tenant lifecycle management, work order routing, and accounting reconciliation. Key design drivers: data freshness for leasing decisions, idempotent sync for financial accuracy, and tenant-facing portal responsiveness.
Architecture Diagram
Dashboard (React) ──→ Property Service ──→ Redis Cache ──→ AppFolio Stack API
↓ /properties
Queue (Bull) ──→ Sync Worker /tenants
↓ /leases
Webhook Handler ←── AppFolio Events /work-orders
↓ /bills
Accounting Sync ──→ QuickBooks/Xero
Service Layer
class PropertyService {
constructor(private client: AppFolioClient, private cache: CacheLayer) {}
async getPortfolioSummary(propertyIds: string[]): Promise<PortfolioSummary> {
const properties = await Promise.all(
propertyIds.map(id => this.cache.getOrFetch(`prop:${id}`, () => this.client.get(`/properties/${id}`)))
);
return { totalUnits: properties.reduce((sum, p) => sum + p.units.length, 0),
vacancyRate: this.calcVacancy(properties), pendingWorkOrders: await this.getPendingOrders(propertyIds) };
}
async routeWorkOrder(order: WorkOrderRequest): Promise<string> {
const property = await this.client.get(`/properties/${order.propertyId}`);
const vendor = this.selectVendor(property.region, order.category);
return this.client.post('/work-orders', { ...order, assigned_vendor: vendor });
}
}
Caching Strategy
const CACHE_CONFIG = {
properties: { ttl: 300, prefix: 'prop' }, // 5 min — changes infrequently
tenants: { ttl: 120, prefix: 'tenant' }, // 2 min — moderate churn
leases: { ttl: 60, prefix: 'lease' }, // 1 min — financial accuracy
workOrders: { ttl: 30, prefix: 'wo' }, // 30s — real-time tracking
vacancies: { ttl: 15, prefix: 'vacancy' }, // 15s — leasing speed matters
};
// Webhook-driven invalidation: AppFolio events flush matching cache keys immediately
Event Pipeline
class PropertyEventPipeline {
private queue = new Bull('appfolio-events', { redis: process.env.REDIS_URL });
async onWebhook(event: AppFolioEvent): Promise<void> {
await this.queue.add(event.type, event, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
}
async processLeaseEvent(event: LeaseEvent): Promise<void> {
Apply production-ready patterns for AppFolio REST API integration.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio SDK Patterns
Overview
Production-ready patterns for the AppFolio property management REST API. AppFolio uses HTTP Basic Auth with client credentials and returns JSON responses for properties, tenants, leases, and work orders. A structured singleton client prevents credential sprawl, enforces consistent error handling, and centralizes pagination logic across all property management endpoints.
Singleton Client
import axios, { AxiosInstance } from 'axios';
let _client: AxiosInstance | null = null;
export function getClient(): AxiosInstance {
if (!_client) {
const clientId = process.env.APPFOLIO_CLIENT_ID;
const clientSecret = process.env.APPFOLIO_CLIENT_SECRET;
const baseURL = process.env.APPFOLIO_BASE_URL;
if (!clientId || !clientSecret || !baseURL) throw new Error('APPFOLIO_CLIENT_ID, SECRET, and BASE_URL required');
_client = axios.create({ baseURL, auth: { username: clientId, password: clientSecret }, timeout: 30000 });
}
return _client;
}
Error Wrapper
export class AppFolioError extends Error {
constructor(public status: number, public code: string, message: string) { super(message); }
}
export async function safeCall<T>(operation: string, fn: () => Promise<T>): Promise<T> {
try { return await fn(); }
catch (err: any) {
const status = err.response?.status ?? 0;
if (status === 429) { await new Promise(r => setTimeout(r, 5000)); return fn(); }
if (status === 401) throw new AppFolioError(401, 'AUTH', 'Invalid APPFOLIO_CLIENT_ID or SECRET');
throw new AppFolioError(status, 'API_ERROR', `${operation} failed [${status}]: ${err.message}`);
}
}
Request Builder
class AppFolioQuery {
private params: Record<string, string> = {};
status(s: 'active' | 'past' | 'future') { this.params.status = s; return this; }
propertyId(id: string) { this.params.property_id = id; return this; }
page(n: number) { this.params.page = String(n); return this; }
perPage(n: number) { this.params.per_page = String(Math.min(n, 200)); return this; }
since(date: string) { this.params.updated_since = date; return this; }
build() { return this.params; }
}
// Usage: new AppFolioQuery().status('active').perPage(50).build();
Response Types
interface Property {
id: string; name: string; property_type: 'residential' | 'commercial' | 'mixed';
address: { street: string; city: string; state: string; zip: string };
unit_count: number; status: string;
}
interface Tenant {
id: string; first_name: string; last_name: string;
email: string; phone: string; unit_id: string; lease_id: string;
}
interface Lease {
id: string; unit_id: string; tenant_id: strin
Secure AppFolio API credentials and tenant data.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Security Basics
Overview
AppFolio manages property portfolios containing tenant PII (SSNs, bank accounts, lease terms), owner financial data, and maintenance vendor records. A breach exposes rent rolls, payment histories, and personally identifiable tenant information across every managed property. Secure every integration point: API credentials, webhook endpoints, and any pipeline that touches tenant or owner financial records.
API Key Management
import https from "https";
import axios, { AxiosInstance } from "axios";
function createAppFolioClient(): AxiosInstance {
const clientId = process.env.APPFOLIO_CLIENT_ID;
const clientSecret = process.env.APPFOLIO_CLIENT_SECRET;
const baseUrl = process.env.APPFOLIO_BASE_URL;
if (!clientId || !clientSecret || !baseUrl) {
throw new Error("Missing APPFOLIO_CLIENT_ID, APPFOLIO_CLIENT_SECRET, or APPFOLIO_BASE_URL");
}
return axios.create({
baseURL: baseUrl,
auth: { username: clientId, password: clientSecret },
httpsAgent: new https.Agent({ minVersion: "TLSv1.2", rejectUnauthorized: true }),
});
}
Webhook Signature Verification
import crypto from "crypto";
function verifyAppFolioWebhook(req: Request, res: Response, next: NextFunction): void {
const signature = req.headers["x-appfolio-signature"] as string;
const secret = process.env.APPFOLIO_WEBHOOK_SECRET!;
const expected = crypto.createHmac("sha256", secret).update(req.body).digest("hex");
if (!signature || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
res.status(401).send("Invalid signature");
return;
}
next();
}
Input Validation
import { z } from "zod";
const TenantSchema = z.object({
tenant_id: z.string().uuid(),
first_name: z.string().min(1).max(100),
last_name: z.string().min(1).max(100),
email: z.string().email(),
unit_id: z.string().uuid(),
lease_start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
rent_amount: z.number().positive().max(100000),
});
function validateTenantPayload(data: unknown) {
return TenantSchema.parse(data);
}
Data Protection
const APPFOLIO_PII_FIELDS = ["ssn", "bank_account", "routing_number", "date_of_birth", "drivers_license"];
function redactAppFolioLog(record: Record<string, unknown>): Record<string, unknown> {
const redacted = { ...record };
for (const field of APPFOLIO_PII_FIELDS) {
if (field in redacted) redacted[field] = "[REDACTED]";
}
return redacted;
}
Security Checklist
- [ ] API credentials stored in secrets manager, not
.env in production
- [ ] HTT
Migrate between AppFolio API versions and handle endpoint changes.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Upgrade & Migration
Overview
AppFolio property management integrations depend on versioned REST API endpoints that
evolve with the platform. Upgrades can rename fields on property and tenant objects,
change pagination models, deprecate work-order endpoints, and alter the basic-auth
flow. This skill detects your current API version, maps deprecated response shapes
to replacements, and rolls back automatically if the new version fails.
Prerequisites
- Current API version prefix documented (e.g.,
/api/v1/)
- Access to the AppFolio API changelog and release notes
- Staging environment with test property and tenant data
- Client ID and secret stored in environment variables
- Existing integration test suite that covers core endpoints
Instructions
- Run version detection to compare your active API version against the latest.
- Review the AppFolio changelog for breaking changes between the two versions.
- Apply schema migration transforms to property and tenant response objects.
- Update endpoint URLs from the old version prefix to the new one.
- Switch pagination from offset to cursor-based if required by the new version.
- Run the smoke test suite against staging to verify all endpoints respond.
- Deploy to production with the rollback strategy enabled.
- Monitor error logs for 410/401 responses indicating missed migration steps.
Output
After a successful migration the skill produces:
- A
VersionInfo object confirming current, latest, and deprecated versions
- Transformed property and tenant objects matching the new schema
- Smoke test results for properties, tenants, work orders, and accounting endpoints
- Rollback log entries if any endpoint fell back to the previous version
Version Detection
interface VersionInfo { current: string; latest: string; deprecated: string[]; }
async function detectApiVersion(baseUrl: string, headers: Record<string, string>): Promise<VersionInfo> {
const res = await fetch(`${baseUrl}/api/status`, { headers });
const body = await res.json();
const current = res.headers.get("X-AppFolio-Api-Version") ?? body.api_version;
const deprecated: string[] = body.deprecated_versions ?? [];
if (deprecated.includes(current)) {
console.warn(`Version ${current} is deprecated. Migrate to ${body.latest_version}.`);
}
return { current, latest: body.latest_version, deprecated };
}
Schema Migration
interface LegacyProperty { address_line1: string; unit_count: number; mgr_id: string; }
interface CurrentProperty { street_address: string; total_units: number; manager_id: string; }
function migrateProperty(old: LegacyProperty): CurrentP
Handle AppFolio webhook events for property management notifications.
ReadWriteEditBash(npm:*)Bash(curl:*)Grep
AppFolio Webhooks & Events
Overview
AppFolio Stack delivers real-time webhook notifications for property management lifecycle events including tenant onboarding, lease execution, rent payments, and maintenance workflows. Use these webhooks to sync AppFolio data with your CRM, accounting system, or custom property management dashboards without polling the API.
Webhook Registration
const response = await fetch("https://api.appfolio.com/v1/webhooks", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.APPFOLIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://yourapp.com/webhooks/appfolio",
events: ["tenant.created", "work_order.updated", "payment.received", "lease.signed"],
secret: process.env.APPFOLIO_WEBHOOK_SECRET,
}),
});
Signature Verification
import crypto from "crypto";
import { Request, Response, NextFunction } from "express";
function verifyAppFolioSignature(req: Request, res: Response, next: NextFunction) {
const signature = req.headers["x-appfolio-signature"] as string;
const expected = crypto
.createHmac("sha256", process.env.APPFOLIO_WEBHOOK_SECRET!)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: "Invalid signature" });
}
next();
}
Event Handler
import express from "express";
const app = express();
app.post("/webhooks/appfolio", express.raw({ type: "application/json" }), verifyAppFolioSignature, (req, res) => {
const event = JSON.parse(req.body.toString());
res.status(200).json({ received: true });
switch (event.type) {
case "tenant.created":
syncTenantToCRM(event.data.tenant_id, event.data.property_id); break;
case "work_order.updated":
notifyMaintenanceTeam(event.data.work_order_id, event.data.status); break;
case "payment.received":
recordPayment(event.data.lease_id, event.data.amount_cents); break;
case "lease.signed":
activateLease(event.data.lease_id, event.data.move_in_date); break;
}
});
Event Types
| Event |
Payload Fields |
Use Case |
tenant.created |
tenantid, propertyid, email |
Sync new tenant to CRM |
work_order.updated |
workorderid, status, assigned_vendor |
Dispatch or escalate maintenance |
Ready to use appfolio-pack?
|
|
|