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)

The personal access token needs the data.records:read, data.records:write, schema.bases:read, and schema.bases:write scopes: every sync run verifies (and if needed creates) the schema before writing records.

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 8 tables in Airtable:

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

Note

Tenancies and Tenancy Balances are two views of the same PropertyMe entity (same IDs): Tenancies carries the lease agreement fields, Tenancy Balances carries the financial fields. Both are synced so each is available as real columns.

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

Snapshot-First Fetch

Every run starts by fetching all PropertyMe bulk endpoints into one in-memory snapshot, before anything is written to Airtable. If any fetch fails, the run aborts with the base untouched and the CLI exits non-zero.

Multi-Phase Sync

  1. Phase 0: Fetch the PropertyMe snapshot (all-or-nothing)
  2. Phase 1: Ensure the Airtable schema is current
  3. Phase 2: Sync core tables (Contacts, Members, Properties) without linked records
  4. Phase 3: Build ID mappings (PropertyMe ID → Airtable Record ID)
  5. Phase 4: Sync dependent tables with linked record resolution
  6. Phase 5: Re-sync Properties to add Owner/Tenant/Manager links
  7. Phase 6: 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:

  • Stale properties are replaced from the snapshot's archived properties, and stale tenancy rows are flagged closed (or, for Tenancy Balances, replaced) from the snapshot's balances
  • Other tables fetch each stale record's current state from PropertyMe by ID; 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")