appfolio-api-sdk
v0.3.9
Published
TypeScript SDK for the AppFolio Property Manager API
Maintainers
Readme
AppFolio API SDK
A fully typed TypeScript SDK for the AppFolio Property Manager API.
This SDK provides access to two separate AppFolio APIs:
- Data API (v0) - CRUD operations for properties, units, tenants, work orders, etc.
- Reports API (v2) - Financial and operational reports (rent roll, income statement, GL, etc.)
Installation
npm install @yourorg/appfolio-api
# or
pnpm add @yourorg/appfolio-api
# or
bun add @yourorg/appfolio-apiQuick Start
Data API
import { AppFolio } from '@yourorg/appfolio-api'
// Initialize the Data API client
const appfolio = new AppFolio({
clientId: process.env.APPFOLIO_CLIENT_ID!,
clientSecret: process.env.APPFOLIO_CLIENT_SECRET!,
developerId: process.env.APPFOLIO_DEVELOPER_ID!,
})
// Start making API calls!
const properties = await appfolio.properties.list()
console.log(properties)Reports API
import { AppFolioReports } from '@yourorg/appfolio-api'
// Initialize the Reports API client
const reports = new AppFolioReports({
clientId: process.env.APPFOLIO_CLIENT_ID!,
clientSecret: process.env.APPFOLIO_CLIENT_SECRET!,
database: 'yourcompany', // yourcompany.appfolio.com
})
// Get rent roll
const rentRoll = await reports.rentRoll({
as_of_to: '2024-01-31',
})
// Get income statement
const income = await reports.incomeStatement({
posted_on_to: '2024-01',
})Usage
Properties
// List all properties
const properties = await appfolio.properties.list()
// List with filters
const multiFamilyProperties = await appfolio.properties.list({
propertyType: 'Multi-Family',
limit: 50,
})
// Get a specific property
const property = await appfolio.properties.get('property-uuid')
// Search properties
const results = await appfolio.properties.search('123 Main St')
// Get all properties (warning: can be slow on large datasets)
const allProperties = await appfolio.properties.all()Units
// List units
const units = await appfolio.units.list()
// List units for a specific property
const propertyUnits = await appfolio.units.list({
propertyId: 'property-uuid',
})
// Filter by status
const vacantUnits = await appfolio.units.list({
status: 'Vacant',
acceptingApplications: true,
})
// Get a specific unit
const unit = await appfolio.units.get('unit-uuid')
// Get all units for a property
const units = await appfolio.units.forProperty('property-uuid')
// Get available units with filters
const available = await appfolio.units.available({
propertyId: 'property-uuid',
minBedrooms: 2,
maxRent: 2000,
})Tenants
// List current tenants
const currentTenants = await appfolio.tenants.list({
status: 'Current',
})
// Get tenants for a specific unit
const unitTenants = await appfolio.tenants.list({
unitId: 'unit-uuid',
})
// Get a specific tenant
const tenant = await appfolio.tenants.get('tenant-uuid')
// Update tenant custom fields
await appfolio.tenants.update('tenant-uuid', {
occupancyCustomFields: {
customField1: 'value',
},
})Work Orders
// List work orders
const workOrders = await appfolio.workOrders.list()
// Filter by status
const urgentWorkOrders = await appfolio.workOrders.list({
status: 'New',
propertyId: 'property-uuid',
})
// Get a specific work order
const workOrder = await appfolio.workOrders.get('work-order-uuid')
// Create a work order
const newWorkOrder = await appfolio.workOrders.create({
propertyId: 'property-uuid',
jobDescription: 'Fix leaky faucet in unit 201',
unitId: 'unit-uuid',
vendorId: 'vendor-uuid',
priority: 'Urgent',
type: 'Resident',
scheduledStart: '2024-01-20T09:00:00Z',
scheduledEnd: '2024-01-20T12:00:00Z',
})Leads
// List leads
const leads = await appfolio.leads.list()
// Filter by property
const propertyLeads = await appfolio.leads.list({
propertyId: 'property-uuid',
status: 'active',
})
// Get a specific lead
const lead = await appfolio.leads.get('lead-uuid')
// Create a lead
const newLead = await appfolio.leads.create({
firstName: 'John',
lastName: 'Doe',
propertyId: 'property-uuid',
email: '[email protected]',
phoneNumber: '555-1234',
bedrooms: 2,
maxRent: '2000',
desiredMoveIn: '2024-02-01',
source: 'Website',
message: 'Interested in 2BR units',
})
// Update a lead
await appfolio.leads.update('lead-uuid', {
status: 'waitlisted',
assignedUserId: 'user-uuid',
})Bills
// List bills
const bills = await appfolio.bills.list()
// Filter by approval status
const pendingBills = await appfolio.bills.list({
approvalStatus: 'Pending Approval',
})
// Get a specific bill
const bill = await appfolio.bills.get('bill-uuid')
// Create a bill
const newBill = await appfolio.bills.create({
propertyId: 'property-uuid',
vendorId: 'vendor-uuid',
dueDate: '2024-02-01',
invoiceDate: '2024-01-15',
totalAmount: '500.00',
lineItems: [
{
amount: '500.00',
description: 'Plumbing repair',
glAccountId: 'gl-account-uuid',
},
],
reference: 'INV-001',
})
// Update a bill
await appfolio.bills.update('bill-uuid', {
dueDate: '2024-02-15',
remarks: 'Payment extended',
})Vendors
// List vendors
const vendors = await appfolio.vendors.list()
// Get a specific vendor
const vendor = await appfolio.vendors.get('vendor-uuid')
// Get all vendors
const allVendors = await appfolio.vendors.all()GL Accounts
// List GL accounts
const accounts = await appfolio.glAccounts.list()
// Filter by corporate accounts only
const corporateAccounts = await appfolio.glAccounts.list({
isCorporateAccount: true,
})
// Get a specific GL account
const account = await appfolio.glAccounts.get('account-uuid')
// Get all GL accounts
const allAccounts = await appfolio.glAccounts.all()
// Bulk create GL accounts
const result = await appfolio.glAccounts.bulkCreate([
{
referenceId: 'ref-1',
name: 'Operating Account',
type: 'cash',
number: '1000',
},
{
referenceId: 'ref-2',
name: 'Rent Income',
type: 'income',
number: '4000',
},
])
// List GL details (transaction-level data)
const transactions = await appfolio.glAccounts.listDetails({
dateFrom: '2024-01-01',
dateTo: '2024-01-31',
propertyId: 'property-uuid',
accountingBasis: 'Accrual',
})
// Get all GL details for a specific account
const accountTransactions = await appfolio.glAccounts.allDetails({
dateFrom: '2024-01-01',
dateTo: '2024-01-31',
glAccountId: 'account-uuid',
})Owners
// List all owners
const owners = await appfolio.owners.list()
// Get owners for a specific property
const propertyOwners = await appfolio.owners.list({
propertyId: 'property-uuid',
})
// Get a specific owner
const owner = await appfolio.owners.get('owner-uuid')
// Get all owners
const allOwners = await appfolio.owners.all()
// Get all owners for a property (auto-paginated)
const owners = await appfolio.owners.forProperty('property-uuid')Reports API (v2)
The Reports API provides access to financial and operational reports.
Rent Roll Reports
import { AppFolioReports } from '@yourorg/appfolio-api'
const reports = new AppFolioReports({
clientId: process.env.APPFOLIO_CLIENT_ID!,
clientSecret: process.env.APPFOLIO_CLIENT_SECRET!,
database: 'yourcompany',
})
// Standard Rent Roll
const rentRoll = await reports.rentRoll({
as_of_to: '2024-01-31',
unit_visibility: 'active',
non_revenue_units: '0',
})
// Commercial Rent Roll
const commercial = await reports.rentRollCommercial({
as_of_to: '2024-01-31',
include_vacant: '1',
})
// Itemized Rent Roll (with GL account breakdown)
const itemized = await reports.rentRollItemized({
as_of_to: '2024-01-31',
gl_account_ids: ['1', '2', '3'],
})
// Filter by properties
const filtered = await reports.rentRoll({
as_of_to: '2024-01-31',
properties: {
properties_ids: ['1', '2'],
property_groups_ids: ['10'],
},
})Income Statement Reports
// Standard Income Statement (by month)
const income = await reports.incomeStatement({
posted_on_to: '2024-01', // YYYY-MM format
accounting_basis: 'Cash',
level_of_detail: 'detail_view',
})
// Comparative Income Statement (vs prior year)
const comparative = await reports.incomeStatementComparative({
posted_on_from: '2024-01',
posted_on_to: '2024-03',
accounting_basis: 'Accrual',
})
// Property Comparison (side-by-side)
const comparison = await reports.incomeStatementPropertyComparison({
posted_on_from: '2024-01',
posted_on_to: '2024-03',
properties: {
properties_ids: ['1', '2', '3'],
},
})
// Custom Date Range (YYYY-MM-DD format)
const dateRange = await reports.incomeStatementDateRange({
posted_on_from: '2024-01-01',
posted_on_to: '2024-01-15',
})General Ledger
const gl = await reports.generalLedger({
posted_on_from: '2024-01-01',
posted_on_to: '2024-01-31',
accounting_basis: 'Accrual',
exclude_zero_dollar_receipts_from_cash_accounts: '1',
})
// Filter by GL accounts
const filtered = await reports.generalLedger({
posted_on_from: '2024-01-01',
posted_on_to: '2024-01-31',
gl_account_ids: ['100', '200'],
})Bill Detail
const bills = await reports.billDetail({
occurred_on_from: '2024-01-01',
occurred_on_to: '2024-01-31',
payment_status: 'Unpaid',
date_type: 'Bill Date',
})
// Filter by vendor
const vendorBills = await reports.billDetail({
occurred_on_from: '2024-01-01',
occurred_on_to: '2024-01-31',
party_contact_info: {
vendor_id: '123',
},
})Paginated Results
All report methods automatically fetch all pages. For large datasets, use the paginated variants:
// Get first page only
const firstPage = await reports.rentRollPaginated({
as_of_to: '2024-01-31',
})
console.log(firstPage.results) // Array of rows
console.log(firstPage.next_page_url) // URL for next page (or null)Configuration
Environment Variables
Create a .env file in your project:
# Data API Credentials
APPFOLIO_CLIENT_ID=your-client-id
APPFOLIO_CLIENT_SECRET=your-client-secret
APPFOLIO_DEVELOPER_ID=your-developer-id
# Reports API Credentials (may be different from Data API)
APPFOLIO_REPORTS_CLIENT_ID=your-reports-client-id
APPFOLIO_REPORTS_CLIENT_SECRET=your-reports-client-secret
APPFOLIO_DATABASE=yourcompanyNote: Some AppFolio accounts use the same credentials for both APIs, others have separate Reports API credentials. Check with your AppFolio account administrator.
Next.js Example
// lib/appfolio.ts
import { AppFolio } from '@yourorg/appfolio-api'
export const appfolio = new AppFolio({
clientId: process.env.APPFOLIO_CLIENT_ID!,
clientSecret: process.env.APPFOLIO_CLIENT_SECRET!,
developerId: process.env.APPFOLIO_DEVELOPER_ID!,
})// app/api/properties/route.ts
import { appfolio } from '@/lib/appfolio'
export async function GET() {
const properties = await appfolio.properties.list()
return Response.json(properties)
}// app/api/units/[id]/route.ts
import { appfolio } from '@/lib/appfolio'
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const unit = await appfolio.units.get(params.id)
return Response.json(unit)
}TypeScript Support
This SDK is fully typed. All API responses, request parameters, and enums are typed:
import type { Property, Unit, Tenant, WorkOrder } from '@yourorg/appfolio-api'
// TypeScript will autocomplete and validate
const properties: Property[] = await appfolio.properties.list({
propertyType: 'Multi-Family', // ✓ Autocompletes valid property types
// propertyType: 'Invalid', // ✗ TypeScript error
})API Rate Limits
AppFolio enforces the following rate limits:
- 8 requests per second
- 256 requests per minute
- 4096 requests per hour
The SDK handles pagination automatically but does not implement rate limiting. You should implement your own rate limiting if making many requests.
Error Handling
try {
const property = await appfolio.properties.get('invalid-id')
} catch (error) {
if (error instanceof Error) {
console.error('API Error:', error.message)
// "Property not found: invalid-id"
}
}Development
# Install dependencies
pnpm install
# Build
pnpm build
# Watch mode
pnpm dev
# Generate API documentation
pnpm docs
# Lint and format
pnpm check:fixDocumentation
This SDK is fully typed with TypeScript, providing auto-completion and type checking in your IDE.
API Reference
Auto-generated API documentation is available by running:
pnpm docsThis generates a complete API reference website in the docs/ directory using TypeDoc. Open docs/index.html in your browser to view it.
Type Definitions
All types are exported and fully documented:
import type { Property, Unit, Tenant, WorkOrder } from '@yourorg/appfolio-api'TypeScript will provide autocomplete and validation for all API methods, parameters, and return types.
Testing
This SDK includes comprehensive unit and integration tests.
Quick Start
# Run all tests
pnpm test
# Run only unit tests (no credentials needed)
pnpm test:unit
# Run integration tests (requires credentials)
pnpm test:integration
# Watch mode
pnpm test:watch
# Coverage
pnpm test:coverageIntegration Tests
Integration tests require real AppFolio credentials in .env.test:
# Copy the example
cp .env.test.example .env.test
# Edit with your credentials
# APPFOLIO_CLIENT_ID=...
# APPFOLIO_CLIENT_SECRET=...
# APPFOLIO_DEVELOPER_ID=...
# APPFOLIO_DATABASE=yourcompany
# Run integration tests
pnpm test:integrationNote: Integration tests only perform read operations (GET/list) and never create, update, or delete data.
See TESTING.md for detailed testing documentation.
License
MIT
