Recipe

Export units for one property nightly to CSV

Problem

A downstream system — a BI tool, a partner feed, a spreadsheet someone refuses to give up — wants a nightly CSV of every unit in a single property. You want a stable file, generated on a schedule, with no manual steps.

This recipe is the do-it-yourself path: the public REST API plus a scheduler you already run (cron, a Celery beat task, a GitHub Action). If you'd rather PropSocket own the schedule and delivery, use managed CSV export Automationsinstead. Reach for this recipe when you want full control over the file shape, the transport, or a destination automations don't deliver to.

Code

Filter GET /v1/units by property_id, page to the end, and write the rows. Sort by created_at:asc so the page boundaries stay stable as units are added.

export_units.py / export_units.mjs
import csv
import os

import requests

BASE = "https://api.propsocket.io/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PROPSOCKET_API_KEY']}"}
PROPERTY_ID = os.environ["PROPERTY_ID"]  # the one property you're exporting


def all_units(property_id):
    offset, limit = 0, 100  # 100 is the max page size
    while True:
        resp = requests.get(
            f"{BASE}/units",
            headers=HEADERS,
            params={
                "property_id": property_id,
                "limit": limit,
                "offset": offset,
                "order-by": "created_at:asc",  # stable order across pages
            },
        )
        resp.raise_for_status()
        body = resp.json()
        yield from body["results"]
        if not body["meta"]["hasMore"]:
            return
        offset += limit


def export(property_id, out_path):
    fields = ["id", "x_id", "unit_number", "building", "status", "type", "bedrooms", "bathrooms"]
    with open(out_path, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        for unit in all_units(property_id):
            writer.writerow(unit)


if __name__ == "__main__":
    export(PROPERTY_ID, "units.csv")

Schedule it however you run jobs. A bare crontab entry:

crontab
# /etc/cron.d/propsocket-units-export — runs at 02:15 every night.
15 2 * * *  deploy  PROPSOCKET_API_KEY=ps_test_… PROPERTY_ID=prp_… /usr/bin/python3 /opt/jobs/export_units.py >> /var/log/units-export.log 2>&1

Why this works

  • property_id is a supported filter on the Unit list endpoint, so the API does the narrowing — you never download units you'll discard. See the full filter set inFiltering & sorting.
  • Paging until meta.hasMore is false with created_at:ascgives a deterministic walk: new units appended at the end never shift earlier pages. The pagination contract is in Pagination.
  • Soft-deleted units are excluded by default, so your nightly file naturally drops units that left the source. Pass ?include_deleted=true only if you specifically want a tombstone of what was removed.

What to watch out for

  • Rate limits are per-Organization. A single property's units fit in a handful of pages, but if you schedule many exports at the same minute they share the 150 req/min pool. Stagger the cron entries or honor Retry-After on a 429 — seeRate limits.
  • Cache freshness, not real time. You're reading PropSocket's normalized cache, which refreshes on your tier's sync cadence. A 2 AM export reflects the most recent sync, not the live PMS. If you need change-driven freshness, pair this with theUNIT_UPDATED webhook instead — webhooks are on theScale plan and above.
  • Quote your CSV fields. Unit numbers and building names can contain commas. The Python csv module handles this; the Node sample includes a minimal escaper. Ruby's CSV, PHP's fputcsv, Go's encoding/csv, and .NET handle quoting too.
  • Keep secrets out of the crontab if you can. The inline env vars above are for illustration — prefer an EnvironmentFile or a secrets manager in production. SeeLocal development for the .env pattern.

Next

To dump every entity (not just one property's units), useBackfill after downtime, which generalizes the same paginate-and-write loop across all four entities.