# Native appraisal sync for Node.js

Copy your dealership's saved native appraisals into a **private** SQLite database. This receiver reads inspection facts, seller answers and linked-stock detail; it does not create an appraisal, execute a payment or download files.

## Start

1. Use Node.js **22.13 or newer**. Save `signal-trade-stock-sync.mjs` and `signal-trade-native-appraisal-sqlite-example.mjs` together.
2. In Business → Connections, choose Appraisals → **Manage inspections and purchase decisions**. Keep `SIGNAL_TRADE_API_KEY` in the server environment. Ordinary `appraisal:workflow` permission is enough; private file transfer remains separately opt-in.
3. Run `node signal-trade-native-appraisal-sqlite-example.mjs`.

The result reports the committed record count and opaque checkpoint, without printing seller facts or the bearer token. The default database is `signal-native-appraisals.db`. Set `SIGNAL_TRADE_SQLITE_PATH` for another location and `SIGNAL_TRADE_BASE_URL` only when deliberately using another approved server.

The old `appraisal:read` and `appraisal:manage` scopes are for safe saved figures, calculation and figure import. They cannot discover or read private native inspections. An `appraisal:workflow_evidence` key can also run this mirror, but **the mirror never calls file endpoints**. Keep the database and backups on trusted storage; these facts include private seller contacts and notes.

## Keep it current

Run the command again for changes. It runs once per invocation; your receiving system chooses the schedule within the API's hourly limits. Full reconciliation happens on the first run and at least daily **when invoked**. Nothing installs a background scheduler.

| Option | Use |
| --- | --- |
| `--status` | Read local counts, binding and checkpoint without a key or API request. |
| `--reconcile` | Replace from the complete current inventory and catch up changes now. |
| `--rebind` | Deliberately accept a changed key/scope for the same dealership and fully rebuild. |

For example: `node signal-trade-native-appraisal-sqlite-example.mjs --reconcile`.

## Recovery and storage

The database binds to the normalized API origin, business ID, key ID, non-secret key prefix and exact scope. Another business or server needs a separate database; `--rebind` does not permit changing either. Existing unbound native tables need a deliberate `--rebind` before replacement. The API key is never written into SQLite or status output.

One `BEGIN IMMEDIATE` transaction with WAL and full synchronization covers record replacement, change hydration, removals, binding and checkpoint. A second writer fails rather than racing the first. Readers keep the previous committed dataset while the writer runs. Every failed request, malformed page, callback failure, authorization loss or interruption rolls back the current run. The final account check occurs immediately before commit. Retry after resolving the problem; failed callbacks must be safe to repeat.

Python's native appraisal mirror uses the same two-table format and state names. Stock mirrors use separate tables and a different default database. Protect any older backups and exports yourself; rebuilding the live mirror does not erase them.

## Client helpers

| Node helper | Job |
| --- | --- |
| `getNativeAppraisal({apiKey, appraisalId, baseUrl})` | Read exact current private facts, with no file bytes. |
| `getNativeAppraisals({apiKey, ids, baseUrl})` | Read one to ten complete current envelopes in requested order, all or nothing. |
| `loadAllNativeAppraisals({apiKey, onAppraisal, baseUrl})` | Discover every compact page, hydrate exact records and return `{syncCheckpoint, recordsUpdated}`. |
| `syncNativeAppraisalRecords({apiKey, checkpoint, onRecord, onDelete, saveCheckpoint, baseUrl})` | Drain a fixed sequence window, hydrate touched IDs and save the checkpoint last. |

`baseUrl` defaults to `https://signalautomotive.uk`. Full loading does **not** save a checkpoint: take its `syncCheckpoint`, then catch up the feed before committing. The runnable example supplies that transaction. Custom callbacks must durably and idempotently upsert by `detail.appraisal.id`, remove by appraisal ID, and commit records plus checkpoint together. Each callback receives exact detail (the complete current response envelope); event callbacks also receive the latest notice for that ID.

The helpers validate the full current-ID batch before callbacks. Every touched ID is resolved, even when its last notice is `delete`: a current record wins after deletion and recreation. An absent record can be evicted only if the completed window contains an explicit delete notice. Other missing IDs, changed detail, repeated IDs/cursors or nonterminal exact-ID batches fail without saving the checkpoint. Deletion may mean physical removal **or current source visibility loss**; do not treat it as an instruction to delete anything in Signal.

Pages are limited to 100 records. The reference client permits at most 100,000 discovered/touched IDs and 1,000,000 notices per run; exceeding either stops without checkpointing. A larger receiver needs an appropriately bounded durable staging design. Sequence checkpoints are opaque, bound to the business/key/scope, and distinct from the old timestamp-based figure feed. `pollStartedAt` is informational. A current inventory plus catch-up is recoverable, not a historical point-in-time snapshot. Periodic complete reconciliation prunes omissions only after success.

## Batches and API allowance

Discovery and presence pages contain at most 100 summaries. Full detail hydration uses `/api/v1/appraisals/native/batch?ids=...`, at most ten complete envelopes per request. Any missing, invalid, imported, unavailable or raced member fails the whole batch. The SDK verifies the complete ordered response before callbacks and never falls back to individual GETs on errors. The 1 MiB cumulative data cap returns 413; deliberately set `SIGNAL_TRADE_NATIVE_BATCH_SIZE` to 1–9 and retry the unchanged checkpoint. Custom helpers accept `detailBatchSize` (default 10). A single record exceeding the cap still needs review.

The mirror shares `createNativeAppraisalSyncTransport({apiKey, baseUrl})` across **all** account, discovery, batch, presence, event and retry requests. Its monotonic-clock pacing starts GETs at least 600 ms apart (at most 100/minute for this receiver). Custom receivers should share that transport too, via the helpers' `transport` option and `getTradeApiAccount({..., transport})`. The transport accepts a fake `now`/`sleep` or explicit `requestSpacingMs` for isolated fixtures. CLI spacing below 600 ms is permitted only for loopback fixtures; production defaults remain paced. It contains no durable key storage or logging.

The network limit remains 120 requests/minute and the **shared key** remains limited to 600 GETs/hour. Pacing does not expand either quota. A 1,000-record full inventory with batches of 10 requires at least 113 requests: 10 inventory pages, 100 detail batches, one catch-up page and two account checks. Add changed-ID presence/hydration, extra event pages and retries. Daily full reconciliation needs its own allowance, including other systems using the key. The 100,000-record/1,000,000-event safeguards are **memory ceilings, not throughput promises**. Histories exceeding the available hourly budget need a custom durable staging receiver across quota windows; this atomic reference mirror keeps its old copy after rate failure and does not resume a partly committed full load.

These examples do not establish integration with an external dealer system, valuation accuracy, inspection completeness or legal acceptance. Review your destination mapping and privacy/retention needs before relying on its copy.
