# Signal 0.1.0 — purchase and sale connection

Save the Node.js client or Python client from the API guide. Both use the free Signal Trade v1 interface. A current approved dealership principal creates a dedicated **Purchase and sale records** connection in Business → Connections.

Choose `deal:read` for agreements, counterparty contact facts, recorded money, handover and saved history. Choose `deal:workflow` when the trusted system also records agreements, reservations, releases, payments and completed handovers as the principal. Existing stock, finance, appraisal and CRM permissions do not grant this access. These scopes do not transfer private files or acknowledgements.

Keep the key in trusted server secret settings. Send it over HTTPS in the Authorization header. Confirm the business ID, key ID and exact scope using `getTradeApiAccount` / `get_trade_api_account` before binding your local storage. A key replacement or permission change needs a deliberate inventory rebuild; a different dealership needs separate storage.

## Start and change a record

Every deliberate write uses a fresh UUID v4 `operationId`. Persist the exact body, target, method and original key identity **before sending**. Helpers do not generate IDs or automatically repeat writes.

| Task | Node.js helper | Python helper |
| --- | --- | --- |
| Current detail | `getDealRecord` | `get_deal_record` |
| Start draft | `createDealRecord` | `create_deal_record` |
| Change workflow | `changeDealRecord` | `change_deal_record` |
| Record payment event | `recordDealPayment` | `record_deal_payment` |
| Copy inventory | `loadAllDeals` | `load_all_deals` |
| Drain changes/removals | `syncDealRecords` | `sync_deal_records` |
| Exact saved revision | `getDealSnapshot` | `get_deal_snapshot` |

Create sends `operationId`, `kind` (`purchase` or `sale`) and exactly one `vehicleId` or `sourceAppraisalId`. A sale requires stock. A purchase can start from its buying appraisal and retain one identity through a later full inspection and stock conversion.

Changes send `operationId`, current integer `expectedVersion` and `action`: `save_agreement`, `save_handover`, `reserve`, `release_reservation`, `cancel` or `complete`. Agreement and handover objects replace their complete respective facts; read current detail before preparing a deliberate revision. Omitted top-level objects remain unchanged.

Reserve/release/complete and cancellation of a reserved sale also need `expectedVehicleUpdatedAt`, `expectedCostVersion` and the current `expectedAppraisalUpdatedAt` when linked. Read these from the current `source` objects. A release needs a reason, keeps the agreement and payments and restores the genuine prior stock stage. An earlier advertised stage restores to retail ready with the advert paused. A reservation deadline is recorded context; expiry does not automatically release, cancel or refund.

Recorded transaction money is a tuple: integer GBP `amountPence`, explicitly recorded `vatBasis` and applicable `vatRateBasisPoints`. A rate of 2000 represents 20%. Draft unknowns remain unknown; the connection must not infer VAT recovery or margin eligibility from vehicle type. The portal keeps its invoice-total default. Completion separately requires the actual acquisition/sale date and handover facts, current buying readiness, any vehicle restrictions and the resolved recorded payment balance.

Payment events use `record`, `clear`, `dispute` or `void`, with `kind` (`deposit`, `payment`, `refund`), date and state. `record` adds the amount once. A state change targets the current last event in that movement and omits its amount. Dispute and void require a reason. Pending and disputed records do not clear a balance. Recording money does not execute a transfer.

## Link a part-exchange

The same `deal:read` and `deal:workflow` permissions cover the linked exchange. Read
`GET /api/v1/deals/{saleDealId}/part-exchange`; use
`?view=candidates&search=registration&limit=25` for a bounded incoming purchase/appraisal search.
Review the incoming appraisal through its existing buying workflow and create its canonical purchase.
Use `POST /api/v1/deals/{saleDealId}/part-exchange` with the saved `operationId`, both deal IDs,
`sameCounterpartyConfirmed: true`, and the current pins in the shipped OpenAPI specification.
An original seller enquiry ID is optional and must actually connect the same incoming appraisal and outgoing vehicle.

The pair response contains separate `sale` and `purchase` deal details, `partExchange.settlement`
and neutral history. The full allowance is the incoming invoice customer gross. Each invoice retains
its independently recorded VAT tuple. Actual cleared cash remains on its real deal:
the non-cash allocation is the smaller remaining invoice amount after prior cash.
`netCashPence` is positive when the customer owes the dealer and negative when the dealer owes the customer.
Use the `reconciled` and `valid` flags as well as the amount; net zero by itself does not establish reconciliation.
Copy each source revision stamp unchanged from the fresh read. Vehicle and appraisal stamps may
use ISO or retained SQLite UTC text; do not normalize those bytes before sending the expected pins.

Read `GET /api/v1/part-exchanges/{pairId}` before any deliberate change. Ordinary linked payment,
reservation and handover helpers also require `expectedPartExchangeVersion` plus all current
`expectedSaleVersion`, `expectedPurchaseVersion` and both source pins. Their replies include
`partExchangeDetail` with both updated legs. Record a real payment once on its actual invoice;
never create a mirrored payment on the other leg.

Complete through `PATCH /api/v1/part-exchanges/{pairId}` with `action: "complete"`, the current pair
`expectedVersion`, both current deal/source pins, actual `saleDate` and `acquisitionDate` for unfinished
legs, and their actual saved handovers. This commits both outcomes together. A previously completed
incoming purchase preserves its original acquisition and handover. Single-leg completion is blocked.
The same endpoint supports reasoned `correct`, `unlink`, `cancel` and manager/principal `reverse`.
A manager or principal can record a reasoned settlement reversal after completion. It deactivates
the non-cash allocation, preserves original invoices and vehicle outcomes, and leaves retained actual cash to reconcile separately;
it does not record physical vehicle return or automatically reopen stock.

Persist method, exact target, body and original key identity before sending paired requests directly
from the trusted server. Retain the original returned pair operation receipt separately from fresh
current detail. Validate receipt ID/action/pair ID, committed pair and leg versions, event ID and time.
After an uncertain reply, deliberately repeat only that identical request and operation ID.
A definite conflict requires fresh guarded reads and a new reviewed operation; never automatically change the body.
The Node/Python ordinary deal helpers continue to accept these extra linked pins. Pair endpoints are
documented HTTP operations; this release does not add automatic pair retry or a pair synchronization daemon.

## Resolve an uncertain response

Retry the identical persisted request with the **same** operation ID and original active connection. A changed request, other key or portal/API collision returns a conflict. The returned `operation` identifies the original `committedVersion`, `snapshotId` and `committedAt`; the outer current detail can be newer.

For example, the original operation can commit revision 3, be retried after someone creates revision 5, and return both the revision 3 receipt and current revision 5 detail. Do not substitute current detail for the original receipt when deciding whether that action committed. Replaying a release does not release a later new reservation.

```js
import { readFile } from 'node:fs/promises';
import { changeDealRecord } from './signal-trade-stock-sync.mjs';

// Your trusted server saved this request before the first send.
const saved = JSON.parse(await readFile('./pending-deal-request.json', 'utf8'));
const result = await changeDealRecord({
  apiKey: process.env.SIGNAL_TRADE_API_KEY,
  dealId: saved.dealId,
  body: saved.body,
});
// Verify and retain result.operation; apply result.deal as current facts.
// Do not print the key or private customer details into application logs.
```

```python
import json
import os
from signal_trade_stock_sync import change_deal_record

with open("pending-deal-request.json", encoding="utf-8") as source:
    saved = json.load(source)
result = change_deal_record(
    os.environ["SIGNAL_TRADE_API_KEY"], saved["dealId"], saved["body"]
)
# Retain result["operation"] separately from result["deal"].
```

A stale revision or current source conflict needs a fresh GET and deliberate reconciliation with a **new** operation ID. An uncertain transport/server response retains its original request for exact retry. Current access is still required for historical receipts.

## Build a complete local copy

1. Verify and bind the account identity. Stage a full `loadAllDeals` / `load_all_deals` inventory with no filters. Callbacks must durably store each record before returning. The helper rejects duplicate IDs, repeated cursors and a changed checkpoint. A failed inventory must not replace the last complete copy or label a partial copy complete.
2. Commit the completed inventory and returned `syncCheckpoint` together. For rich source/payment detail, fetch each current record with the detail helper. Query filters are useful for views; a filtered inventory is not a full business mirror.
3. Drain `syncDealRecords` / `sync_deal_records` from that saved checkpoint. Record/removal callbacks must be durable and idempotent. Save the new checkpoint only through `saveCheckpoint` / `save_checkpoint`, which runs after all pages and callbacks succeed.

The helper uses the same checkpoint on every cursor page, fetches each distinct changed deal once per page, applies removals without a private-parent lookup, then saves the terminal checkpoint. A crash or callback failure leaves the checkpoint unchanged, so repeating the poll is safe when your writes are idempotent.

Notices are ID-only and survive deal/vehicle deletion. A notice can also report `delete` when a retained deal becomes unavailable after a source transfer or change. Later visibility restoration sends an upsert. The event action describes current authorised visibility, so a removal notice does not claim the underlying row was physically erased. Business closure removes its retained notices. No historical pre-migration notices are invented; the initial inventory covers those records.

The feed uses a business-scoped sequence and a fixed upper window. `pollStartedAt` is informational. Use the opaque `syncCheckpoint` / terminal `nextCheckpoint`; do not advance using device time or an event timestamp.

## History and limits

Current detail provides the newest 100 saved revision summaries. Use `/deals/ID/snapshots` to page full saved revisions (default 25, maximum 100), `?version=N` to read an exact version, or the exact snapshot helper for a known ID. Exact history stays available beyond the newest summaries, subject to current source access. Full paired historical revisions and their documents require current finance access to both parents, including after unlinking or cancellation. Ordinary own-invoice summaries omit the other invoice's paired settlement. The saved content digest is an integrity identifier; it is not a signature or certification.

Read limit: 600 requests/hour per key. Write limit: 120 requests/hour per key. Responses are private and not cacheable. A response above 4 MiB fails explicitly; request smaller history pages. A deal supports at most 1000 recorded payment events. An integration records declared facts and uses the same completion rules as the portal; it does not execute payments, publish external adverts or grant access to licensed provider data.

The [OpenAPI specification](https://signalautomotive.uk/openapi/signal-trade-v1.yaml) contains route, body and response details. The [API guide](https://signalautomotive.uk/motor-trade/api#purchase-sale-records) provides the short setup flow.
