Skip to content

Airtable Sync

Sync PropertyMe data to Airtable for custom views, reporting, and integrations.

Installation

uv add pypropertyme[airtable]

Configuration

Set these environment variables (in .env or your shell):

Variable Description
AIRTABLE_API_TOKEN Airtable personal access token
AIRTABLE_BASE_ID Airtable base ID (e.g., appXXXXXXXXXXXXXX)

Quick Start

# 1. Set environment variables
export AIRTABLE_API_TOKEN="patXXX..."
export AIRTABLE_BASE_ID="appXXX..."

# 2. Initialize Airtable base with schema
pypropertyme-airtable init

# 3. Sync data
pypropertyme-airtable sync

# 4. Check status
pypropertyme-airtable status

CLI Commands

pypropertyme-airtable init

Creates all required tables and fields in an Airtable base.

pypropertyme-airtable init

Note

The Airtable base must already exist. Create one at airtable.com.

pypropertyme-airtable sync

Syncs active records from PropertyMe to Airtable.

pypropertyme-airtable sync [--dry-run] [--table <TABLE>]
Option Description
--dry-run Preview sync without writing to Airtable
--table Sync specific table only (e.g., Contacts, Properties)

pypropertyme-airtable status

Shows table statistics and record counts.

pypropertyme-airtable status

pypropertyme-airtable test-connection

Tests connections to both PropertyMe and Airtable.

pypropertyme-airtable test-connection

Tables

The sync creates 7 tables in Airtable:

Table Description Primary Field
Contacts Owners, tenants, suppliers Name
Members Agency staff Name
Properties Managed properties Reference
Tenancy Balances Financial data (rent, arrears, bond) Label
Jobs Maintenance jobs Summary
Inspections Property inspections Summary
Tasks Tasks and reminders Summary

Note

Tenancy Balances is a superset of tenancy data, containing all tenancies (active and closed) plus financial balance information. A separate Tenancies table is not needed.

Field Naming

All PyPropertyMe-managed fields use the PME prefix to distinguish them from user-created fields:

  • PME PropertyMe ID - Unique identifier (upsert key)
  • PME Name, PME Email, etc. - Data fields
  • PME JSON Data - Complete raw JSON from PropertyMe (see below)

JSON Data Field

Every table includes a PME JSON Data field containing the complete JSON payload from PropertyMe. This provides:

  • Access to fields not explicitly mapped to Airtable columns
  • Future PropertyMe API fields without schema updates
  • Raw data for custom integrations or debugging
  • Complete data for export/backup purposes

Info

JSON is formatted with 2-space indentation for readability, except for Inspections which uses minified JSON due to Airtable field size limits.

How It Works

Multi-Phase Sync

  1. Phase 1: Sync core tables (Contacts, Members, Properties) without linked records
  2. Phase 2: Build ID mappings (PropertyMe ID → Airtable Record ID)
  3. Phase 3: Sync dependent tables with linked record resolution
  4. Phase 4: Re-sync Properties to add Owner/Tenant/Manager links
  5. Phase 5: Refresh stale records

Upsert Logic

Uses Airtable's batch_upsert with PME PropertyMe ID as the key field. Records are created if new, updated if existing.

Stale Record Refresh

After syncing, records in Airtable that weren't included in the PropertyMe sync response are refreshed individually:

  • For each stale record, the sync fetches its current state from PropertyMe
  • Records that return 404 (deleted from PropertyMe) are logged and skipped
  • Records that have become archived/closed are updated with their true state

Schema Evolution

The implementation supports adding new fields to existing tables.

Supported:

  • Adding new fields to existing tables

Not supported:

  • Modifying existing field types or options
  • Removing fields no longer in the schema
  • Renaming fields

Workflow for adding new fields:

# 1. Add new field definition to schema.py
# 2. Re-run init to add missing fields
pypropertyme-airtable init

# 3. Update transformer in transformers.py to populate the new field
# 4. Run sync to populate data
pypropertyme-airtable sync

Module Structure

src/pypropertyme/sync/airtable/
├── __init__.py      # Module exports
├── cli.py           # CLI commands (pypropertyme-airtable)
├── creator.py       # Table/field creation logic
├── schema.py        # Table schema definitions
├── sync.py          # Core sync logic
└── transformers.py  # PyPropertyMe model → Airtable record transformers

Programmatic Usage

from pypropertyme.client import Client
from pypropertyme.sync.airtable import AirtableSync

# Create PropertyMe client first
pme_client = Client.get_client(token)

# Create sync instance
sync = AirtableSync(pme_client, "patXXX...", "appXXX...")

# Initialize schema (not async)
sync.init_base()

# Run sync
await sync.sync_all()

# Sync specific table
await sync.sync_table("Contacts")