AppFolio — Property & Tenant Management
Overview
Primary workflow for AppFolio property management integration. Covers the full property
lifecycle: creating and updating property records, managing tenant profiles and lease
agreements, and querying occupancy data. Uses the AppFolio Stack API with OAuth 2.0
client credentials for server-to-server access. All endpoints return JSON and support
pagination via cursor parameters for large portfolios.
Prerequisites
- A current AppFolio partner/API contract that confirms the assigned portfolio base URL, authentication method, approved scopes, and write capabilities; do not infer these from a tutorial or another portfolio.
- A separate sandbox with disposable properties, units, and synthetic tenant records, plus a credential owner and an approved request budget.
- Idempotency keys and an operator-approved rollback/reconciliation process for property, tenant, and lease writes, which can create real legal and billing consequences.
Instructions
- Confirm the provider-issued base URL and authentication flow for the target portfolio before copying any client configuration into an environment.
- Exercise the create/update path only with sandbox synthetic fixtures and idempotency keys; retrieve canonical records before retrying an uncertain write.
- Validate ownership, lease dates, and unit availability before creating a lease, and emit only record IDs and redacted status evidence.
- Promote a write-capable flow only after a staged rehearsal proves rollback, reconciliation, and the least-privilege scope boundary.
Step 1: Authenticate and Initialize Client
const token = await fetch('https://api.appfolio.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.APPFOLIO_CLIENT_ID!,
client_secret: process.env.APPFOLIO_CLIENT_SECRET!,
scope: 'properties tenants leases',
}),
}).then(r => r.json());
const headers = { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/json' };
Step 2: Create or Update a Property
const property = await fetch('https://api.appfolio.com/v1/properties', {
method: 'POST', headers,
body: JSON.stringify({
name: 'Sunrise Apartments',
address: { street: '100 Main St', city: 'Austin', state: 'TX', zip: '78701' },
type: 'residential',
units: [
{ number: '101', bedrooms: 2, bathrooms: 1, rent: 1450 },
{ number: '102', bedrooms: 1, bathrooms: 1, rent: 1100 },
],
}),
}).then(r => r.json());
console.log(`Property created: ${property.id}`);
Step 3: Add a Tenant and Lease
const tenant = await fetch('https://api.appfolio.com/v1/tenants', {
method: 'POST', headers,
body: JSON.stringify({
first_name: 'Test', last_name: 'Tenant',
email: 'test-tenant@example.invalid', phone: '555-0100',
}),
}).then(r => r.json());
await fetch('https://api.appfolio.com/v1/leases', {
method: 'POST', headers,
body: JSON.stringify({
property_id: property.id, unit_number: '101',
tenant_id: tenant.id, start_date: '2026-05-01', end_date: '2027-04-30',
monthly_rent: 1450, security_deposit: 1450,
}),
}).then(r => r.json());
Step 4: Query Occupancy
const units = await fetch(
`https://api.appfolio.com/v1/properties/${property.id}/units?status=vacant`,
{ headers },
).then(r => r.json());
console.log(`Vacant units: ${units.data.length} of ${units.meta.total}`);
Error Handling
| Issue | Cause | Fix |
|---|---|---|
401 Unauthorized |
Expired or invalid token | Re-authenticate with client credentials |
404 Not Found |
Wrong property/tenant ID | Verify resource ID exists before referencing |
422 Unprocessable |
Missing required fields | Check errors[] array in response body |
409 Conflict |
Duplicate lease for unit | Query existing leases before creating |
429 Too Many Requests |
Rate limit exceeded | Back off using Retry-After header value |
Output
A successful run creates a property with units, adds a tenant, binds them via a lease, and reports vacancy counts. Console output confirms each resource ID on creation.
Examples
For a lease-creation change, run against a named synthetic sandbox unit and tenant with a stable idempotency key. Read the property and current leases, perform the candidate write once, then read back the resulting lease and record only IDs, dates, and the reconciliation status. Re-run the same request to prove it does not create a second lease. If the provider contract, endpoint, authorization, or prior-write outcome cannot be verified, stop before any write and send the case to the portfolio operator for reconciliation.
Resources
Next Steps
Continue with appfolio-core-workflow-b for maintenance requests and payment tracking.