GET /api/v1/sync/changes — Incremental Dataset Changes
Fetch only the records that changed in a dataset since a given cursor, enabling lightweight delta sync without re-downloading full snapshots.
The /api/v1/sync/changes endpoint returns a ordered list of change events that occurred in a dataset after the point represented by a since cursor. This is the second half of the two-phase sync pattern: you call /api/v1/sync/snapshot once to load all current records and obtain an initial cursor, then poll /api/v1/sync/changes on a schedule to apply only the rows that have been inserted, updated, or deleted since your last poll. Each response includes a latest_cursor — replace your stored cursor with this value after every successful call so your next request picks up exactly where this one left off.Each change object carries an operation field (upsert, delete, or supersede) and a payload containing the full new record state for upserts. For deletions, payload is null and you should remove the record identified by record_id from your local store.
All requests must include your API key in the x-api-key header. The required access tier matches that of the corresponding snapshot endpoint for the requested dataset.
The dataset to fetch changes for. Must be one of the valid dataset keys:finance_fees, finance_products, finance_eligibility, equipment_pricing, utility_territory, electricity_rates, hvac_incentives, hvac_climate, hvac_labor, hvac_ahri_matches.See Sync Snapshot for full dataset descriptions.
Cursor string from a previous snapshot or changes response. Use sync.cursor from a snapshot response or latest_cursor from a previous changes response. If you do not have a cursor yet, call /api/v1/sync/snapshot first to obtain one. Passing an invalid or expired cursor returns a 400 error.
Optional trade filter. When provided, only change events for that trade category are returned. Must be one of solar, hvac, roofing, plumbing, or electrical. Should match the trade filter you used when taking the original snapshot.
Maximum number of change records to return per page. The ceiling varies by dataset. When the response sync.has_more is true, follow sync.next_url to retrieve the next page of changes before advancing your cursor.
Each item in the changes array carries an operation field that tells you how to apply it to your local dataset:
Operation
Meaning
Action
upsert
Record was inserted or updated
Insert or update the local record identified by record_id with the data in payload
delete
Record was removed
Delete the local record identified by record_id; payload is null
supersede
Record was replaced by a newer version
Replace the local record identified by record_id with payload; the old version is no longer valid for quoting
Always persist latest_cursor to durable storage before applying changes to your database. If your application crashes mid-apply and you have already advanced the cursor, you will miss those change events. A safe pattern is: store latest_cursor → apply changes in a transaction → commit. On restart, re-fetch changes from the stored cursor; the API is idempotent for the same cursor window.
The cursor representing the newest change included in this response. Store this value and use it as since in your next request. If has_changes is false, latest_cursor equals from_cursor.
true if additional change pages exist beyond this response. Follow next_url to retrieve the next page. Do not advance latest_cursor until has_more is false.
Pre-built URL for the next request. When has_more is false, this URL is still valid — it will return an empty changes response and confirm you are up to date.
async function syncDataset(dataset: string, storedCursor: string) { let cursor = storedCursor; do { const res = await fetch( `https://homeservicedata.com/api/v1/sync/changes?dataset=${dataset}&since=${encodeURIComponent(cursor)}`, { headers: { "x-api-key": process.env.HSD_API_KEY } } ); const { data } = await res.json(); // 1. Persist latest_cursor BEFORE applying changes await db.saveCursor(dataset, data.latest_cursor); // 2. Apply each change to your local store for (const change of data.changes) { if (change.operation === "delete") { await db.delete(dataset, change.record_id); } else { await db.upsert(dataset, change.record_id, change.payload); } } cursor = data.latest_cursor; // Continue if there are more pages } while (data.sync.has_more);}