Recipe

Reconcile after a webhook outage

Availability. Webhooks are on theScale plan and above (Scale and Enterprise). On Starter or Growth you can still read and page through the CDM via the API — see pricing.

Problem

Your webhook receiver was down — a bad deploy, an expired cert, a cloud incident. Events kept flowing. Some retried into your dead-letter queue; some aged out. You're back online and need to guarantee your datastore reflects everything that changed while you were dark, without re-importing the entire dataset.

Option A — Replay from the dashboard (preferred for short outages)

Every delivery is logged in Dashboard → Webhooks → Recent deliveries for 30 days, and failed deliveries sit in the Dead-letter queue with full payloads. If your outage was inside that window, replay the affected deliveries — individually or in bulk. Replays carry the original signature and the original event id, so yourdedupe layer makes overlapping replays harmless.

Option B — Sweep what changed (for long or uncertain outages)

When the outage is longer than you can comfortably replay, or you're not sure exactly what you missed, pull the diff directly from the API: list every entity whose updated_at is at or after your last-good watermark, and apply each record. Include soft-deletes so you also catch records that were removed while you were down.

reconcile.py / reconcile.mjs
import os

import requests

BASE = "https://api.propsocket.io/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PROPSOCKET_API_KEY']}"}
ENTITIES = ["properties", "units", "residents", "leases"]

# The instant your receiver went down. Everything updated at/after this needs
# re-checking. Persist your real "last successfully processed" watermark.
SINCE = "2026-05-20T00:00:00Z"


def changed_since(entity, since):
    offset, limit = 0, 100
    while True:
        resp = requests.get(
            f"{BASE}/{entity}",
            headers=HEADERS,
            params={
                "updated_after": since,        # everything changed since your watermark
                "include_deleted": "true",     # catch records soft-deleted while down
                "limit": limit,
                "offset": offset,
                "order-by": "updated_at:asc",
            },
        )
        resp.raise_for_status()
        body = resp.json()
        yield from body["results"]
        if not body["meta"]["hasMore"]:
            return
        offset += limit


def reconcile():
    for entity in ENTITIES:
        for record in changed_since(entity, SINCE):
            if record.get("deleted_at"):
                apply_soft_delete(entity, record)
            else:
                upsert(entity, record)  # idempotent — safe to overlap with replays


if __name__ == "__main__":
    reconcile()

Why this works

  • A watermark, not a guess. Persisting your last successfully-processedupdated_at (or the timestamp your receiver went down) lets the sweep ask for exactly the window you missed, instead of re-importing everything.
  • include_deleted=true recovers removals. A record soft-deleted while you were offline won't appear in a default list — it's filtered out. Including deletes and checking deleted_at is the only way to learn it left.
  • Sorting by updated_at:asc makes the sweep resumable. If it dies partway, restart from the last updated_at you applied.
  • Idempotent apply. Because both options can deliver the same change twice (replay overlaps with a sweep, or vice-versa), every write must be safe to repeat — the whole reason dedupe comes first.

What to watch out for

  • The 30-day delivery-log window caps Option A. Deliveries older than 30 days aren't replayable. For anything beyond that, the sweep (Option B) or a fullbackfill is the fallback.
  • Mind the rate limit. A wide sweep is request-heavy and shares the 150 req/min per-Organization pool. Honor Retry-After on 429s — seeRate limits.
  • Use UTC ISO 8601 for the watermark. updated_at is UTC; pass yourSINCE as UTC ISO 8601 to avoid an off-by-one-timezone gap.

Next

Reconciliation depends on idempotent processing. For a cold start rather than a recovery, seeBackfill after downtime.