npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

appfolio-api-sdk

v0.3.9

Published

TypeScript SDK for the AppFolio Property Manager API

Readme

AppFolio API SDK

A fully typed TypeScript SDK for the AppFolio Property Manager API.

This SDK provides access to two separate AppFolio APIs:

  1. Data API (v0) - CRUD operations for properties, units, tenants, work orders, etc.
  2. 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-api

Quick 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=yourcompany

Note: 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:fix

Documentation

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 docs

This 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:coverage

Integration 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:integration

Note: Integration tests only perform read operations (GET/list) and never create, update, or delete data.

See TESTING.md for detailed testing documentation.

License

MIT