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