Signal Trade · Developer guide
Your Trade data. Your integration.
Connect your dealership’s stock, appraisals, preparation and customer records to your website or another system. Start with one task and give that connection only the access it needs.
Access and credentials
One revocable key for each connection.
- 01 / CHOOSE
Name the connection and choose its task
In Business → Connections, choose Add connection. Select stock, appraisals, purchase and sale records, preparation, customer records or Signal adverts, then the access that system needs. The dealership principal can create up to five active keys.
- 02 / STORE
Keep the key secret
Signal displays the full key once. Store it in your system’s secret manager. Do not put it in browser code, a URL or public repository.
- 03 / CHECK
Check Signal access
Use Check Signal access while the new key is visible. It confirms the dealership and exact permission attached to that key. Your receiving system still needs its own setup; this check does not prove that it is syncing. Revoke unused keys from Connections.
Authentication and permission details
If creation loses its response, retry the same connection. Signal keeps the original creation reference and returns the saved key details without showing its secret again. If the secret was not received, revoke that key before deliberately creating a replacement.
Authentication
Send the key over HTTPS in the Authorization header:
Authorization: Bearer YOUR_API_KEYFirst confirm which dealership and permission are attached to the key with this read-only connection check:
curl.exe "https://signalautomotive.uk/api/v1/account" -H "Authorization: Bearer YOUR_API_KEY"A successful request returns the business name and ID, connection label, a non-secret key prefix and its exact scope. It does not read vehicle or customer records. Then use only the endpoints allowed by that scope. Replace the placeholder with the key you saved when creating the integration; never paste a real key into browser code or share it in a support message. Keep requests server-to-server so the key stays private. Invalid or revoked keys receive HTTP 401. An advert:manage key can manage Signal advert text, contact details, publication and advert photos for this business; it cannot read stock, finance, CRM or appraisal records. Appraisal read and figure-import keys omit seller contacts, private notes, files and source evidence. Native workflow permissions include private inspection facts; only the explicit appraisal:workflow_evidence permission includes file transfer. CRM keys cannot read or change stock; preparation keys can only read or manage preparation jobs. Read-only and listing-update stock keys cannot create stock records. Costs permissions read the actual stock ledger. The separate purchase and sale permissions include private agreements, counterparty facts and recorded money. The purchase-finance key can update acquisition price, VAT details and date, but cannot change stock stages or sale outcomes.
Publishing stock
Resume an interrupted advert update.
The server example takes stock through photos, price review, publishing and pausing. It saves each request before sending it, so a lost reply can be confirmed safely.
Connection example and developer details
Use stock-management and Signal-advert keys for the same dealership. Review the actual vehicle, asking price, VAT basis and contact sharing before running the example: it publishes a real Signal advert.
Keep keys on your server. Use a private SQLite journal and the same plan to resume. The journal retains the original requests, photo bytes and save confirmations. Current records are fetched separately before preparing each new step.
For stock creation, choose one saved retry identifier: operationId for an original-save confirmation, or legacy createId. Stock updates, advert creation, save/publish/pause and photo reorder/removal accept operationId; uploads retain uploadId. Sale writes retain their separate saleId contract. Each bulk stock row can have its own operation ID.
An identical retry returns the immutable original operation and the freshly authorized current record. Reusing an ID with changed details, another target or a different originating key returns 409. Keep the original body and version when the outcome is uncertain. An old publish confirmation does not republish a subsequently paused advert.
A completed state change, its save confirmation and supported change notice commit together. Revoked keys or lost trade access cannot return saved confirmations or current records. This workflow publishes to Signal; provider publication and licensed data require their own agreements.
Receiving stock
Start with a working stock copy.
For your developer or IT person: the Python example stores stock and branches in a local SQLite database. Python 3.10 or newer is required.
- 01 / DOWNLOAD
Save both files together
- 02 / RUN
Use your read-stock key
Set
SIGNAL_TRADE_API_KEYprivately on the server, then run:python signal_trade_sqlite_example.py - 03 / CHECK
Read the result
A successful run confirms the local stock count and saved checkpoint. Run it again to fetch changes. A failed run keeps the previous complete copy.
The example refreshes the full inventory once a day when you run it. It runs once per command; your receiving system chooses the schedule. A local copy does not automatically publish a website or connect a dealer system.
Recovery and key changes
Use --status to inspect saved sync information, or --reconcile to refresh the full stock copy now. After replacing a key or changing its permission, use --rebind to rebuild the same dealership’s copy. Use a separate database for another dealership or server. An old database without a saved business binding also needs this deliberate rebuild.
Keep the database on trusted server storage. Removing finance permission rebuilds the current mirror without financial fields; it does not erase your own backups or other copies. The Python setup guide covers storage settings and recovery.
Node.js alternative and other downloads
The Node.js stock mirror uses the same SQLite format. It requires Node.js 22.13 or newer. Save its client and mirror together, then run node signal-trade-sqlite-example.mjs.
Full API reference and examples
Native appraisal API
Save an inspection, review the purchase, add it to stock.
Choose Appraisals → Manage inspections and purchase decisions. Enable Include private photos and documents only when the trusted system needs files. This selects appraisal:workflow_evidence; the existing appraisal:workflow permission keeps its facts-only access. Read and figure-import keys keep their current access.
Save the observations
Create a native appraisal or replace its recorded facts. Signal calculates its own damage grade and buying plan from those observations. The connection cannot submit a grade or an invented manager approval.
Record the decision
Request a review, then record the current principal’s proceed, revised-offer or decline decision. Changed inspection or seller evidence requires a fresh review.
Add the purchase to stock
Conversion checks the complete buying workflow, recorded ownership, title, finance and VAT facts. It creates stock and carries across itemised preparation. Converted appraisals remain purchase history.
Keep saved inspections in your system
The Python example copies current inspection facts, seller answers and linked-stock details into a private SQLite database. Save both files together, set SIGNAL_TRADE_API_KEY privately to your workflow key, then run:
python signal_trade_native_appraisal_sqlite_example.pyRun it again for changes. Records and checkpoint commit together; a failed run keeps the previous copy. It refreshes the full inventory daily when invoked and installs no scheduler. Use --status for local counts, --reconcile for a full refresh, or --rebind after a key or scope change for the same dealership. Use a separate database for another dealership.
These records contain private seller and inspection information. Protect local storage and backups. The mirror does not download photos or documents, even with an evidence-enabled key. The old appraisal:read/appraisal:manage figure-import helpers use different endpoints and cannot run this mirror.
Node.js 22.13 or newer can use the equivalent Node client, appraisal mirror and setup guide.
GET /api/v1/appraisals/native?limit=100
GET /api/v1/appraisals/native/events?checkpoint=OPAQUE_CHECKPOINT&limit=100
GET /api/v1/appraisals/native/batch?ids=APPRAISAL_ID1,APPRAISAL_ID2
GET /api/v1/appraisals/native/APPRAISAL_IDDiscovery returns compact records and a pre-load sequence checkpoint. The helpers follow every page, fetch exact current facts, catch up all touched IDs, and save the terminal checkpoint last. Current presence wins after deletion and recreation. A missing record is evicted only with an explicit delete notice; other missing IDs stop the poll. The server's sequence checkpoint is opaque: do not replace it with a timestamp. Custom receivers must durably apply idempotent callbacks and checkpoint in their own transaction, and periodically reconcile the full inventory.
The examples fetch up to ten complete inspections per batch and space all account and native requests at least 600 ms apart. The shared key still has a 600-read hourly limit; allow for other readers, catch-up and daily reconciliation. A 1,000-record full inventory needs at least 113 reads before extra changes or retries. Large histories may need a receiving system with durable staging across quota windows. If a batch exceeds 1 MiB, deliberately reduce SIGNAL_TRADE_NATIVE_BATCH_SIZE to 1–9 and repeat; access or source errors never trigger a silent fallback.
Requests and safe retries
POST /api/v1/appraisals/native
GET /api/v1/appraisals/native/APPRAISAL_ID
PATCH /api/v1/appraisals/native/APPRAISAL_ID
POST /api/v1/appraisals/native/APPRAISAL_ID/review
POST /api/v1/appraisals/native/APPRAISAL_ID/convertEvery write needs a UUID v4 operationId. Updates, reviews and conversion also need the exact current expectedUpdatedAt. After an uncertain reply, repeat the identical body with the same operation ID. A replay returns that operation’s immutable result alongside the currently authorised appraisal; the two can have different revisions. Changed reuse conflicts. For a deliberate edit, reload, reconcile and choose a new operation ID.
Creation and replacement send {operationId, appraisal: {...}}. Replacement also sends expectedUpdatedAt and replaces the complete fact input. Financial input fields such as expectedRetail, offerPrice and estimatedPrep use decimal GBP strings; returned amounts are integer pence. Review uses action: "request" or action: "complete". Completion accepts proceed, revise_offer or decline; a revised offerPricePence is a positive whole-pence amount within the recorded ceiling. Conversion sends only the operation ID and current revision.
The OpenAPI specification describes accepted facts and response shapes. Native detail includes private seller and inspection facts, notes, evidence references and the latest submitted seller condition answers. Seller answers are reported facts, not an independent inspection or legal acceptance. File transfer requires the explicit combined permission. Calculations do not establish independent market evidence, ownership or VAT eligibility, and no licensed provider is called by these endpoints.
Private photo and document transfer
GET /api/v1/appraisals/native/APPRAISAL_ID/files
POST /api/v1/appraisals/native/APPRAISAL_ID/files
GET /api/v1/appraisals/native/APPRAISAL_ID/files/FILE_ID
DELETE /api/v1/appraisals/native/APPRAISAL_ID/files/FILE_IDUse an appraisal:workflow_evidence key. Upload one file as multipart form data with kind (photo or document), a UUID v4 uploadId, the current expectedUpdatedAt, file and an optional supported category. Photos accept JPG, PNG or WebP, up to 8 MB each and 12 per appraisal. Documents accept PDF, JPG or PNG, up to 5 MB each and 10 per appraisal. Downloads need the same bearer key; the returned file URL is private.
Keep the upload ID, bytes, category and revision together until the reply is confirmed. Retry that exact request after an uncertain reply. Removal sends {operationId, expectedUpdatedAt} as JSON. The saved operation records the original result; the current file, removed and appraisalUpdatedAt describe the latest authorised state. Replaying an upload whose file was later removed does not restore it.
Adding or removing a file advances the appraisal revision and sends an existing review back for a fresh decision, including an earlier decline. Reload that revision before the next workflow write. Converted appraisals retain readable file history and exact saved retries, but reject new file changes. Document images cannot be used as inspection-photo references.
Purchase and sale records
Carry the agreement through to handover.
In Connections, choose Purchase and sale records. Read access supplies agreements, recorded payments and saved history. Workflow access also records reservations, releases and completed handovers as the dealership principal. These permissions include private customer and financial facts.
Confirm the agreement
Start from a stock vehicle or buying appraisal. Record the counterparty, agreed amount, VAT basis and transaction dates.
Track the money and reservation
Record deposits, payments and refunds, with their actual clearance status. Reserve an agreed sale or release its reservation with a reason. Recording money does not make a payment.
Complete the handover
Record collection and handover facts. Signal checks the current agreement, balance, buying decision and vehicle restrictions before updating the linked records together.
Requests, history and safe retries
GET /api/v1/deals
POST /api/v1/deals
GET /api/v1/deals/DEAL_ID
PATCH /api/v1/deals/DEAL_ID
POST /api/v1/deals/DEAL_ID/payments
GET /api/v1/deals/DEAL_ID/snapshots
GET /api/v1/deals/DEAL_ID/snapshots/SNAPSHOT_ID
GET /api/v1/deals/eventsEvery write needs a UUID v4 operationId. Changes also need the latest integer expectedVersion. Save the exact request before sending; repeat that request and ID after an uncertain reply. The operation receipt identifies its original committed revision. The outer detail is the current authorised record and may be newer.
A released reservation keeps its agreement and payment history. An expired deadline remains visible until someone deliberately releases or completes the reservation. Release does not refund money or publish an advert. Private files and acknowledgements remain in the Trade portal.
Use the list’s syncCheckpoint before copying its pages, then drain the change feed. Save a new nextCheckpoint only after every page and local update succeeds. The feed includes removal notices and uses sequence checkpoints; device clocks do not decide what gets skipped.
Appraisal API
Calculate, read, import and update appraisals.
Use appraisal:read to calculate Signal trade valuations and sync saved records, or appraisal:manage to import and update records created by your DMS. POST /api/v1/valuation accepts vehicle identity and mileage, then returns a source-labelled retail estimate and buying plan in pence. It uses Signal historical evidence and this dealership’s own recorded outcomes; the API does not call or return connected licensed valuation providers. Estimates can be unavailable for newer, specialist or thinly evidenced vehicles, and are not guaranteed sale or purchase prices. Calculation requests do not create saved appraisal records. The Node.js and Python reference clients include a valuation helper.
POST /api/v1/valuation
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "registration": "AB12CDE", "mileage": 24500, "condition": "good", "sellerType": "private" }With the Node.js client, call calculateTradeValuation:
import { calculateTradeValuation } from "./signal-trade-stock-sync.mjs";
const result = await calculateTradeValuation({
apiKey: process.env.SIGNAL_TRADE_API_KEY,
input: { registration: "AB12CDE", mileage: 24500, condition: "good", sellerType: "private" },
});
console.log(result.valuation.prices, result.valuation.buyingPlan, result.valuation.evidence);With the Python client, call calculate_trade_valuation using an appraisal:read key:
import os
from signal_trade_stock_sync import calculate_trade_valuation
result = calculate_trade_valuation(
os.environ["SIGNAL_TRADE_API_KEY"],
{"registration": "AB12CDE", "mileage": 24500, "condition": "good", "sellerType": "private"},
)
print(result["valuation"]["prices"], result["valuation"]["buyingPlan"])
print(result["valuation"]["evidence"], result["valuation"]["interpretation"])To sync saved appraisals, use appraisal:read. To import externally prepared figures, use appraisal:manage; Signal stores them for review but does not calculate or verify them. Imported records have unscored valuation confidence, no projected contribution and cannot move directly into stock. Review and save them in Signal Trade first. You can also import trim, transmission, drivetrain, door and seat specifications with the vehicle; those recorded details carry through when the appraisal becomes stock.
GET /api/v1/appraisals?limit=100
GET /api/v1/appraisals?updated_since=2026-09-28T12:00:00Z
GET /api/v1/appraisals?ids=ID1,ID2
GET /api/v1/appraisals/events?limit=100
GET /api/v1/appraisals/events?cursor=NEXT_CURSOR
GET /api/v1/appraisals/APPRAISAL_ID
POST /api/v1/appraisals
PATCH /api/v1/appraisals/APPRAISAL_ID{
"createId": "550e8400-e29b-41d4-a716-446655440000",
"source": "Dealer DMS",
"externalReference": "DMS-APP-2048",
"vehicle": { "registration": "AB12CDE", "make": "Example", "model": "Model", "year": 2022, "trimLevel": "SE", "transmission": "automatic", "drivetrain": "front_wheel", "doorCount": "5", "seatCount": "5", "mileage": 24500 },
"figures": { "expectedRetailPence": 1849500, "recommendedPrepPence": 30000, "estimatedPrepPence": 30000, "buyingFeesPence": 15000, "riskReservePence": 10000, "targetGrossPence": 200000, "maximumBuyPence": 1400000, "marginalCeilingPence": 1450000 }
}Use a stable UUID v4 createId and repeat it for retries. An identical retry returns the original record; changed details with the same ID return HTTP 409. Updates need the latest expectedUpdatedAt from the appraisal response. A stale update returns HTTP 409; reload the record and reconcile. The figure-update endpoint updates imported records only. Changes return the record to review, clear projected contribution and keep valuation confidence unscored. For the initial sync, first call GET /api/v1/appraisals/events?limit=1 and save its pollStartedAt. Then load every GET /api/v1/appraisals page without updated_since. After the full list is loaded, drain the event feed using updated_since set to that saved checkpoint. Keep updated_since fixed through every cursor page, fetch changed appraisal IDs in batches of up to 100, and advance the checkpoint to the first page response pollStartedAt only after all changed records are saved. On later polls, send the previous checkpoint as updated_since and advance it to the new response pollStartedAt only after the full poll succeeds. Each event identifies a created or updated appraisal; fetch changed appraisals in batches of up to 100 with GET /api/v1/appraisals?ids=ID1,ID2. Events do not include deleted records or private evidence.
PATCH /api/v1/appraisals/APPRAISAL_ID
{
"expectedUpdatedAt": "2026-09-28T12:00:00.000Z",
"offerPricePence": 1350000,
"estimatedPrepPence": 35000
}Data origin and privacy
Appraisal responses identify imported records and their source reference. The existing summary and figure-import endpoints withhold seller contacts, notes, postcode, evidence and private files. Native inspection facts require a separate workflow permission; private photos and documents also require explicit appraisal:workflow_evidence access.
Preparation API
Keep workshop work linked to stock.
Use a separate prep:read key to sync jobs or prep:manage to create and update them. Jobs remain attached to the same Signal vehicle record; vehicleId comes from the stock API. Read up to 100 jobs per page and use the inclusive updated_since checkpoint for incremental sync.
GET /api/v1/preparation?limit=100
GET /api/v1/preparation?vehicle_id=VEHICLE_ID
GET /api/v1/preparation?ids=ID1,ID2
GET /api/v1/preparation/events?updated_since=2026-09-28T12:00:00Z
GET /api/v1/preparation/JOB_ID
POST /api/v1/preparation
PATCH /api/v1/preparation/JOB_IDBefore initial setup, call GET /api/v1/preparation/events?limit=1 and retain its server-generated pollStartedAt. Finish the full job list, then drain events from that checkpoint. On later polls, use the prior checkpoint as updated_since; the first page returns a new server-generated pollStartedAt that stays fixed across its cursor pages. Advance to that value only after every page succeeds. Deduplicate by event ID and retry from the prior checkpoint if a page fails. Fetch changed jobs in batches of up to 100 with GET /api/v1/preparation?ids=ID1,ID2. The feed contains changes recorded after preparation event auditing is enabled.
Create each job with a stable UUID v4 createId and retry the exact body after an uncertain response. Updates need the current expectedUpdatedAt; include a stable UUID v4 operationId for an exact retry after a later correction. HTTP 409 means the record, source or supplier changed: reload and reconcile. A successful replay returns the current job and its original receipt without repeating a mutation. Costs are whole pence; null means unknown and 0 confirms no cost.
Download the Node.js preparation records guide for create, update and separate rework examples.
Link a job with supplierId from the supplier directory in Signal Trade. The directory is managed in the portal; this V1 API does not expose supplier directory routes. New links require an active supplier from the same business. Saved job and timeline labels retain the name recorded at assignment when the directory is renamed, archived or erased.
{
"createId": "550e8400-e29b-41d4-a716-446655440000",
"vehicleId": "vehicle-record-id",
"category": "mechanical",
"title": "Investigate coolant leak",
"severity": "urgent",
"estimatePence": 18500,
"blocksRetail": true
}For a completed job that needs further repair, create separate rework. Keep the original job and its cost history; the new actual cost starts unknown and no previous cost or supplier assignment is copied. Send the completed source’s exact updatedAt and record a reason.
POST /api/v1/preparation
{
"action": "rework",
"createId": "b08f6287-3a5c-47c0-8eba-92caa81bac18",
"reworkOfJobId": "COMPLETED_JOB_ID",
"expectedUpdatedAt": "2026-10-03T12:00:00.000Z",
"reworkReason": "Fault returned after the recorded repair"
}Job detail includes an append-only events timeline and timing. Start-to-completion and creation-to-completion are elapsed recorded times, including waiting; they are not measured labour. Older jobs without the required recorded steps keep null timing. startedAt and event timestamps are recorded by Signal rather than accepted from the client. prep:read redacts costs, private notes, rework reasons and event reasons; prep:manage includes those fields.
Keep safety and retail holds intact
An open safety-critical job automatically places the vehicle on a do-not-use hold. An open job marked blocksRetail prevents the stock API from moving that vehicle to retail_ready or advertised. The integration can update ordinary job details, severity and restrictions. A rework job retains its recorded severity and use or retail restrictions, including after its original job is erased. Completing a job does not itself certify roadworthiness or clear the vehicle’s safety hold.
CRM API
Sync relationships and follow-ups.
Use crm:read for sync-only access or crm:manage to also create and update records. Choose a record kind: customers, opportunities, tasks or activities. Responses are scoped to the approved business and include record IDs and linked customer or vehicle IDs. Follow pagination.nextCursor to continue a sync.
Fetch an individual record by ID with its kind query parameter; the response stays within the approved business.
The event feed returns record type, ID, action and change time only; it contains no contact details or notes. Stored trigger actions are returned as the public customer, opportunity, task or activity created/updated names. An activity update notice lets a connection refresh its saved record; activity entries still cannot be edited through this API. Use the CRM endpoints to fetch current record details after each event. The assignee directory requires crm:read and returns current dealership user IDs, display names and roles without email addresses. Set ownerUserId on a task with crm:manage, using null to leave it unassigned. Download the Node.js CRM records and tasks guide for ready-to-use directory, create and update helpers.
GET /api/v1/crm?kind=customers&page_size=100
GET /api/v1/crm?kind=customers&page_size=100&cursor=NEXT_CURSOR
GET /api/v1/crm/events?updated_since=LAST_POLL_STARTED_AT
GET /api/v1/crm?kind=tasks&filter=overdue
GET /api/v1/crm/RECORD_ID?kind=customers
POST /api/v1/crm
PATCH /api/v1/crm/RECORD_IDTo book a timed appointment, send a dueDate, dueTime, durationMinutes from 1 to 720 and the current team member’s stable ownerUserId. It must end by midnight on the same Europe/London local date. Signal checks open appointments for the same person across branches and for the same vehicle across people; one appointment may begin exactly when another ends. A conflict returns HTTP 409 CRM_CONFLICT, so reload and choose another slot. An older timed appointment with an unknown length blocks new bookings for that person or vehicle on that day until its length is recorded or it is completed. Unrelated edits and completion preserve that unknown length. Reopening or changing the date, time, branch, person, vehicle or length requires a known length. An untimed task can use an empty dueTime and null duration. Local times are wall-clock values, not UTC instants; a repeated daylight-saving clock hour is conservatively treated as the same slot.
Create records with an entity and a stable UUID createId. Retrying the same details returns the existing record; reusing that ID with different details, or after the saved record has changed, returns HTTP 409 so a client can reload and reconcile. Updates send the record version; stale changes also receive HTTP 409. Activity entries can be added but not edited.
{
"entity": "customers",
"createId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example customer",
"role": "seller",
"phone": "02890000000"
}Example partial update:
{
"entity": "customers",
"version": 3,
"phone": null
}For initial setup, request GET /api/v1/crm/events?limit=1 and retain its server-generated pollStartedAt, complete every page for all four CRM kinds with filter=all, then drain events from that checkpoint. For each later poll, use the prior checkpoint as updated_since; the first page returns a new server-generated pollStartedAt shared by its cursor pages. Keep updated_since fixed and advance the checkpoint to pollStartedAt only after every page succeeds. For each event, map its singular type to the plural CRM kind value (customer → customers, opportunity → opportunities, task → tasks, activity → activities), group events by type, fetch changed records in batches of up to 100 with GET /api/v1/crm?kind=customers&ids=ID1,ID2 (using the matching plural kind), and upsert by ID. Deduplicate by event ID, and do not use response generatedAt as the checkpoint. If a page fails, keep the old checkpoint and retry. Events created during paging are held for the next poll by the server-side time boundary; inclusive checkpoints safely catch timestamp ties. The feed includes CRM changes written after event auditing is enabled; the initial full sync seeds older records. Archived or closed records appear as updates; the CRM API has no hard-delete operation or tombstone feed.
Permission boundary
The CRM key can read contact details, customer preferences and private notes, opportunities, tasks and recorded activity. Treat it as access to confidential business and customer information; store it server-side and revoke it when a connection is no longer needed. It cannot access stock records, appraisal evidence or vehicle photos.
A buyer’s deliberate enquiry on an eligible Signal dealership advert creates a separate customer, selling opportunity, untimed reply task and original activity in the dealer’s CRM. A task or opportunity can return a read-only enquiryActivityId; fetch that exact activity using the same customerId and ids filter. A cleared mapping returns null. The buyer form uses the signed-in website account; it is not a Bearer-key enquiry submission endpoint.
Create stock
Add an acquired vehicle from your system.
Use the Node.js createStockVehicle or Python create_stock_vehicle helper, or send POST /api/v1/stock with a stock:manage key. A unique stock number is required so retries can be checked against the existing business inventory. Include a stable UUID v4 createId to retry safely: identical retries return the original record, and changed details with the same ID return HTTP 409. Without a createId, a repeated stock number or vehicle identity returns HTTP 409 with the existing record ID. For vehicles without a UK registration, supply identityMode: asset_reference and put the serial or chassis reference in registration; the dealer stockNumber stays separate. Omitting identityMode keeps existing behavior: registration when supplied, otherwise stockNumber becomes the asset reference. Responses include the explicit identity mode and reference. A successful HTTP 201 response includes the new vehicle in the same safe stock shape returned by GET. For onboarding inventory, use the Node.js createStockVehicles or Python create_stock_vehicles helper, or send POST /api/v1/stock/bulk with up to 25 vehicles; each row receives the same validation as an individual create.
{
"createId": "550e8400-e29b-41d4-a716-446655440000",
"stockNumber": "ST-2048",
"registration": "AB12CDE",
"make": "Example",
"model": "Model",
"year": 2022,
"trimLevel": "SE",
"transmission": "automatic",
"drivetrain": "front_wheel",
"doorCount": "5",
"seatCount": "5",
"mileage": 24500,
"valuationType": "private",
"askingPricePence": 1849500,
"askingPriceVatBasis": "unknown",
"siteId": "branch-record-id"
}Serial-numbered asset example:
{
"createId": "550e8400-e29b-41d4-a716-446655440001",
"stockNumber": "ST-2049",
"registration": "VIN-EXAMPLE-12345",
"identityMode": "asset_reference",
"make": "Example",
"model": "Tracked carrier",
"year": 2022,
"valuationType": "commercial",
"commercialType": "specialist"
}Batch example:
POST /api/v1/stock/bulk
{
"records": [
{ "createId": "550e8400-e29b-41d4-a716-446655440002", "stockNumber": "ST-2048", "registration": "AB12CDE", "make": "Example", "model": "Model", "year": 2022 },
{ "createId": "550e8400-e29b-41d4-a716-446655440003", "stockNumber": "ST-2049", "registration": "CD34EFG", "make": "Example", "model": "Model", "year": 2021 }
]
}Rows are processed in order and can partially succeed. Check each zero-based index; records retried with their same createId are reported as replayed and return the saved vehicle. Rows without a createId that duplicate an existing stock number or identity return existingId so you can fetch and reconcile them. One batch counts as one stock write request.
For recurring stock syncs, use the Node.js updateStockVehicles or Python update_stock_vehicles helper, or send PATCH /api/v1/stock/bulk with up to 25 update rows. Include each vehicle id plus the same fields accepted by the single-record PATCH route. Normal scope checks, stale expectedUpdatedAt checks and sale safeguards still apply per vehicle. Results identify updated, rejected and not processed rows; reconcile each row before retrying.
PATCH /api/v1/stock/bulk
{
"records": [
{ "id": "stock-record-id-1", "expectedUpdatedAt": "2026-09-28T12:00:00.000Z", "askingPricePence": 1849500, "askingPriceVatBasis": "unknown" },
{ "id": "stock-record-id-2", "expectedUpdatedAt": "2026-09-28T12:00:00.000Z", "status": "advertised" }
]
}For commercial stock, set valuationType to commercial and provide a supported commercialType such as van, lorry, pickup or specialist.
Safe starting state
New records always start in preparation with road status attention. Review their roadworthiness before moving them on. Price and VAT details are validated together; a supplied branch must be active and belong to your business. API-created vehicles do not include a sale record, customer details, private notes or evidence files.
Signal advert API
Manage Signal listings from another system.
Create and update Signal marketplace adverts with a separate advert:manage key. Drafts belong to the dealership and use its business contact details. The key cannot read stock, finance, CRM or appraisal records. New Stock drafts copy the exact recorded asking amount, VAT basis and rate. Existing advert prices are kept. Saving details does not make them public: review the price, add vehicle photos, then publish with explicit confirmation and the current advert version.
Endpoints: GET/POST/PATCH /api/v1/stock/VEHICLE_ID/advert, POST/PATCH /api/v1/stock/VEHICLE_ID/advert/photos and DELETE /api/v1/stock/VEHICLE_ID/advert/photos/PHOTO_ID.
Upload photos as multipart form data with a file field and a stable UUID v4 uploadId. After an uncertain reply, resend the exact file with the same ID. A replay returns the original receipt and current advert; a deliberately removed photo stays removed. Reusing an ID for another file or advert conflicts. Omitting the ID creates a fresh upload. JPG, PNG and WebP are accepted up to 4 MB each, with a maximum of eight photos. Reorder by sending every photo ID exactly once; the first photo becomes the cover. The advert save action also accepts optional `videoUrl` and `spinUrl` HTTPS links (up to 2048 characters); these appear as seller-supplied links on the public Signal advert and are not embedded or verified. Stale advert versions return HTTP 409 so the integration can reload and reconcile.
Signal checks the vehicle details, reviewed asking-price interpretation, site publication setting, contact fields, public description and at least one vehicle photo before publishing. Read currentSourcePrice from the owner advert GET. On save send priceVatBasis, priceVatRateBasisPoints, priceConfirmed: true and that exact expectedSourcePrice. Unknown VAT can remain a private draft; plus or normal VAT needs an explicit rate. A changed Stock asking tuple requires review, but never automatically reprices a live advert. An intentional advert price override is permitted; a margin-scheme choice does not establish eligibility or recoverable VAT. Changes appear in the stock change-event feed when connected with a separate stock:read key. These routes manage Signal adverts only; they do not post to Auto Trader or BCA.
Separate permission
The advert key manages Signal advert content, photos, video and 360° links only. It cannot read stock, financial details or customer records.
Update permissions
Change operational listing fields or manage stock stages.
Send a PATCH request to /api/v1/stock/VEHICLE_ID. A stock:read_write key can update branch, mileage and asking price. A stock:manage key can also update vehicle and specification details, dealer-recorded running dates, service schedule and operational stages. The Node.js updateStockVehicle and Python update_stock_vehicle helpers require the current updatedAt on every ordinary update; use recordStockSale or record_stock_sale with a stable saleId for completed sales. Assign a branch with its stable siteId from the response’s sites list. Send null to unassign it. Existing integrations can continue to use an exact active siteName.
{
"expectedUpdatedAt": "2026-09-28T12:00:00.000Z",
"siteId": "branch-record-id",
"status": "advertised",
"valuationType": "commercial",
"commercialType": "van",
"colour": "White",
"trimLevel": "SE",
"transmission": "automatic",
"drivetrain": "front_wheel",
"doorCount": "5",
"seatCount": "5",
"mileage": 25100,
"askingPricePence": 1849500,
"askingPriceVatBasis": "unknown"
}Accepted fields
siteId must identify an active branch in the response’s sites list. Alternatively, siteName must exactly match a configured active branch. mileage is a whole number of miles or null. Management keys can also update year, colour, fuel, engine, trimLevel, transmission, drivetrain, doorCount, seatCount, valuationType and commercialType. Null resets enum specifications to unknown and clears trim; commercial stock requires a supported category such as van, lorry, pickup or specialist. Price is an integer in pence (maximum £1,000,000) and must be sent with its VAT basis and, where applicable, VAT rate in basis points. Send null to clear a price.
For every non-sale update, send expectedUpdatedAt from the current stock response to prevent a stale integration value from replacing a newer dealer edit. If the timestamp no longer matches, Signal returns HTTP 409 STOCK_CHANGED; reload and reconcile the intended changes. Only stock:manage keys can set a stock stage or record a sale. stock:finance_manage updates the purchase amount, VAT basis/rate and acquisition date as one request with the current expectedUpdatedAt; it cannot update other listing fields, stock stages or sale outcomes. An open preparation job marked as a retail blocker prevents moving stock into retail_ready or advertised; Signal returns HTTP 409 RETAIL_BLOCKERS_OPEN until those jobs are complete. Use removed to withdraw a record from active stock without deleting it. Set status to sold with a stable UUID v4 saleId, soldPricePence, soldDate and soldPriceVatBasis; supply a rate in basis points where needed. Retry the identical request with the same saleId after a timeout. Optional fields are disposalRoute, conditionAtSale and mileageAtSale. These details are dealer-entered and are not asserted as independent evidence. Sold records cannot be edited through the API. Stored purchase costs and sale fields in GET responses are available to stock:finance_read and stock:finance_manage keys only; a sale-write response echoes the sale details sent with that request. Customer data, private notes and private appraisal evidence remain outside the Stock API. Stock removal or sale pauses a published Signal advert. Each successful change updates the vehicle timestamp.
Sync acquisition costs
Keep purchase economics in step.
A stock:finance_manage key can create or correct the purchase price, VAT details and acquisition date. Send all four fields together with the current expectedUpdatedAt. This permission cannot change vehicle stages, sale results, stock listing fields or vehicle identity.
PATCH /api/v1/stock/VEHICLE_ID
{
"expectedUpdatedAt": "2026-09-28T12:00:00.000Z",
"purchasePricePence": 1125000,
"purchasePriceVatBasis": "margin_scheme",
"purchasePriceVatRateBasisPoints": 2000,
"acquisitionDate": "2026-09-27"
}Stale and changed records
If expectedUpdatedAt is stale, Signal returns HTTP 409 STOCK_CHANGED; fetch the current record and reconcile the cost before retrying. Clearing the cost requires purchasePricePence and acquisitionDate set to null, VAT basis unknown and rate null. If a prior structured purchase outcome exists, changing its source facts records an invalidated replacement. The new integration values remain dealer-entered and do not become independently verified evidence.
Record a sale
Keep the completed outcome with the stock record.
A stock:manage key can mark unsold stock sold; sold records are locked. Read the current stock record first and send its expectedUpdatedAt with the actual sale date, price and VAT basis. Signal freezes the recorded cost position at sale, including any missing costs. Record purchase and sale VAT independently from the transaction evidence. Margin-scheme gross requires a reviewed eligibility reference in Costs.
PATCH /api/v1/stock/VEHICLE_ID · {"status":"sold","expectedUpdatedAt":"2026-09-27T09:00:00.000Z","saleId":"550e8400-e29b-41d4-a716-446655440000","soldPricePence":1795000,"soldPriceVatBasis":"vat_not_applicable","soldDate":"2026-09-27","disposalRoute":"retail_consumer","conditionAtSale":"good","mileageAtSale":25100}How Signal treats the figure
The API stores a completed sale in the vehicle record and creates an outcome entry for the business. Details sent by an integration are dealer-entered; they are not independently evidenced and are not automatically treated as verified market evidence. A sold vehicle and its published Signal advert cannot be restored through this API.
Request and response
Read your stock in pages of up to 100.
Use the cursor from one response to request the next page. Records are ordered by most recently updated first. Each response also includes the active branches configured in Trade. A cursor is valid only for the same integration key and filters; keep them unchanged until its page chain completes. Use the ids filter to fetch up to 100 known stock records in one request, such as the IDs from a change-feed page, instead of making one request per vehicle.
GET /api/v1/stock?limit=100
GET /api/v1/stock?limit=100&cursor=NEXT_CURSOR
GET /api/v1/stock?status=advertised&site_id=branch-record-id
GET /api/v1/stock?ids=VEHICLE_ID_1,VEHICLE_ID_2
GET /api/v1/stock/VEHICLE_IDThe single-record request returns the same vehicle fields, branch list and published Signal advert details as the stock list. Filter with an exact status stage or active site_id; use site_id=unassigned for vehicles with no branch. For incremental reads, record a sync-start timestamp and pass it as updated_since on every cursor page. The timestamp is inclusive, so keep the same checkpoint through the full cursor chain, deduplicate by record ID and save it only after every page finishes.
For example: /api/v1/stock?updated_since=2026-09-28T12%3A00%3A00.000Z
Response example
Stable JSON fields for stock sync.
{
"version": "1",
"data": [{
"id": "vehicle-record-id",
"registration": "AB12CDE",
"identity": { "mode": "registration", "reference": "AB12CDE" },
"stockNumber": "ST-1001",
"siteName": "Belfast",
"siteId": "site-record-id",
"make": "Example",
"model": "Model",
"year": 2022,
"colour": "Blue",
"fuel": "Petrol",
"engine": "1.5",
"trimLevel": "SE",
"transmission": "automatic",
"drivetrain": "front_wheel",
"doorCount": "5",
"seatCount": "5",
"mileage": 24500,
"valuationType": "commercial",
"commercialType": "van",
"status": "advertised",
"roadStatus": "road_ready",
"askingPricePence": 1850000,
"askingPriceVatBasis": "unknown",
"askingPriceVatRateBasisPoints": null,
"taxStatus": "",
"taxDueDate": "",
"motStatus": "Valid",
"motExpiryDate": "2027-05-10",
"serviceIntervalMiles": 10000,
"serviceIntervalMonths": 12,
"lastServiceDate": "2026-04-12",
"lastServiceMileage": 22000,
"nextServiceDate": "",
"nextServiceMileage": null,
"insuranceExpiryDate": "",
"leaseEndDate": "",
"marketplaceAdvert": {
"id": "listing-record-id",
"url": "https://signalautomotive.uk/cars/listing-record-id",
"pricePence": 1849500,
"vat": "no_vat",
"priceVatBasis": "vat_not_applicable",
"priceVatRateBasisPoints": null,
"kind": "car",
"bodyType": "hatchback",
"transmission": "automatic",
"videoUrl": "",
"spinUrl": "",
"expiresAt": "2026-10-28T12:00:00.000Z",
"photos": [{ "id": "photo-record-id", "position": 0, "url": "https://signalautomotive.uk/api/marketplace/listing-record-id/photos/photo-record-id" }]
},
"updatedAt": "2026-09-28T12:00:00.000Z"
}],
"sites": [{ "id": "site-record-id", "name": "Belfast" }],
"pagination": { "limit": 100, "nextCursor": null },
"generatedAt": "2026-09-28T12:00:01.000Z"
}Sync stock changes
Keep connected inventory current.
The stock, appraisal, preparation and CRM change feeds are pull-based. Choose a poll cadence that suits your system and the API’s 600-read-request hourly limit, then fetch the changed records identified by each feed. For quicker notification, principal-managed signed webhooks send minimal events on a scheduled run of about once a minute; they are not instant and may arrive more than once.
For a first connection, call GET /api/v1/stock/events?limit=1 and retain its server-generated pollStartedAt; download the complete stock list without updated_since, then drain events from that checkpoint. For later syncs, poll events from the saved inclusive checkpoint. Follow each cursor until nextCursor is null; fetch the current state of all affected vehicle IDs in batches using GET /api/v1/stock?ids=ID1,ID2, remove an absent vehicle locally only when this poll includes its deletion event, and save the server-returned pollStartedAt only when every page succeeds. Each event response bounds its cursor chain to that fixed poll time.
Get server checkpoint: GET /api/v1/stock/events?limit=1
Initial stock load: GET /api/v1/stock?limit=100
Drain changes: GET /api/v1/stock/events?updated_since=POLL_STARTED_AT&limit=100
Batch-fetch changed vehicles: GET /api/v1/stock?ids=ID1,ID2,ID3
Later poll: GET /api/v1/stock/events?updated_since=LAST_POLL_STARTED_AT&limit=100
Next event page: GET /api/v1/stock/events?updated_since=LAST_POLL_STARTED_AT&limit=100&cursor=NEXT_CURSOREvent actions have fixed entity types: Signal advert/media actions use stock_vehicle, vehicle stock, sale and deletion actions use vehicle, and branch changes use business. The Node and Python reference clients reject unsupported or mismatched events. Each client validates fetched batches against all affected IDs before running callbacks. A present current record is upserted even if the poll includes an earlier deletion, so recreation is retained. An absent record can be deleted locally only when the completed poll contains a deletion event for it. Other missing, duplicated, unexpected or invalid records fail the poll and leave its checkpoint unchanged. The change feed returns record IDs and action names only. Fetch changed records in batches of up to 100; they use the same permission-filtered stock response as a normal list. A vehicle with status removed is still an existing stock record and is returned by the normal stock endpoint; it is not a deletion. Periodically perform a full stock reconciliation to find older deletions. CRM records have no hard-delete operation; use the CRM API with filter=all to sync archived or closed records as status changes.
Checkpoint rule
Keep updated_since fixed while paging, and do not advance it if an event page fails partway through. Use the pollStartedAt value returned by the first page as the next checkpoint. Replaying the same checkpoint is safe when your system upserts and deletes by Signal record ID. The feed does not include registrations, customer details or deleted record contents.
Every page must include pagination.nextCursor. Only an explicit null ends its page chain; the reference clients stop if a cursor is missing, invalid or repeated. Treat cursors as opaque values and keep all filters unchanged, including the complete ids list. An IDs lookup can span several pages when you choose a smaller limit.
Signed webhooks
Get small change notices automatically.
In Trade settings, the dealership principal can create up to five HTTPS destinations and choose stock, appraisal, preparation or CRM changes. Signal posts an event ID, event name, record ID, source action and event time. It does not send seller or customer details, notes, photos or documents in webhook payloads; fetch permitted data separately using a scoped API key. Stock imports through the CSV workflow also emit a stock.vehicle.created notice with the stock_csv_imported source action. Send a signed test event from Trade settings to confirm that your receiver accepts Signal’s signature before relying on live changes. The test event has no record ID and does not depend on your selected change groups.
Event names
- Stock and branch changes:
stock.vehicle.created,stock.vehicle.updated,stock.vehicle.deleted,stock.sites.changed
Fetch current records withGET /api/v1/stock?ids=… (vehicle events). Remove deleted vehicles locally. For stock.sites.changed, reload the stock list and its active site list. - Appraisals:
appraisal.created,appraisal.updated,appraisal.converted
Fetch current records withGET /api/v1/appraisals?ids=…. Fetch the current appraisal record. - Preparation:
preparation.job.created,preparation.job.updated
Fetch current records withGET /api/v1/preparation?ids=…. Fetch the current preparation job. - CRM:
crm.customer.created,crm.customer.updated,crm.opportunity.created,crm.opportunity.updated,crm.task.created,crm.task.updated,crm.activity.created,crm.activity.updated
Fetch current records withGET /api/v1/crm?kind=…&ids=…. Use the plural kind, such as customers or tasks.
{
"id": "event-record-id",
"type": "stock",
"event": "stock.vehicle.updated",
"recordId": "vehicle-record-id",
"action": "stock_vehicle_updated",
"occurredAt": "2026-09-30T12:00:00.000Z"
}A test event uses the same signature and delivery headers, and follows the scheduled delivery cadence (about once a minute):
{
"id": "unique-test-event-id",
"type": "test",
"event": "webhook.test",
"recordId": null,
"action": "test",
"occurredAt": "2026-09-30T12:00:00.000Z"
}Verify before processing
Signal signs the raw request body using HMAC-SHA256 over timestamp + "." + body. The request includes X-Signal-Webhook-Timestamp, X-Signal-Webhook-Signature: v1=… and X-Signal-Webhook-Delivery. Verify the signature with the one-time signing secret, reject old timestamps and deduplicate deliveries. Failed requests retry with increasing delays for up to eight attempts. Reconcile from the pull change feeds if delivery is delayed or exhausted.
Download dependency-free Node.js receiver (JSONL inbox) →
Download Python receiver with durable SQLite inbox →
The Node example fsyncs verified events to a local append-only JSONL inbox before acknowledging them and binds to loopback by default; the Python example stores verified deliveries in SQLite. Neither example applies events to stock or CRM automatically. Run a single Node receiver process per inbox file, keep the file on trusted storage and arrange retention; use shared transactional storage when running multiple instances. Expose either receiver only through a public HTTPS endpoint or reverse proxy.
Field and service boundaries
What the API returns.
Vehicle identity and stock details
id, registration, stockNumber, siteName, siteId, make, model, year, colour, fuel, engine, trimLevel, transmission, drivetrain, doorCount, seatCount, mileage, valuationType, commercialType, status, roadStatus and updatedAt. Trim, transmission, drivetrain, door and seat details are dealer-recorded; unavailable values use an empty trim or the unknown category. valuationType distinguishes private and commercial stock; commercialType identifies the commercial category or is not_applicable. siteId is the stable ID of the active assigned branch, or null when none is active. Status filters accept preparation, purchased, transport, inspection, mechanical, bodywork, valet, photography, retail_ready, advertised, reserved, sold and removed.
Asking price and running dates
askingPricePence, askingPriceVatBasis, askingPriceVatRateBasisPoints, dealer-recorded tax status and due date, MOT status and expiry date, service intervals and last/next service dates and mileages, insurance expiry and lease end are available in stock responses and can be set when creating or updating stock with a stock:manage key. Statuses and dates are dealer-recorded; Signal does not independently verify them. If a next-service date or mileage is omitted, Signal calculates it from the supplied service interval and last-service record when enough data is available.
Published advert data and media
marketplaceAdvert contains the Signal public listing URL, advert price/VAT presentation, advert type, body type, transmission, expiry and ordered photo URLs only while the vehicle has a live, published Signal advert. Otherwise it is null. Its pricePence, legacy vat presentation, priceVatBasis and priceVatRateBasisPoints come from the public advert; they may differ from the vehicle’s internal askingPricePence and asking-price VAT basis. These are public advert photos; private appraisal photos and other evidence are never included. The stock API supplies links to Signal adverts and does not publish them to external advertising channels.
Stock API data boundaries
GET stock responses return stored purchase costs, acquisition dates, sale prices, sale dates and their VAT details in the financials object only to stock:finance_read and stock:finance_manage. Other stock keys do not receive those values in GET responses; a stock:manage sale-write response echoes the details sent in that request. No stock key returns customer records, private notes, private appraisal photos or other evidence files. Use a separate crm:read key with GET /api/v1/crm when a system needs customer workspace data. Newly hard-deleted stock records appear in the ID-only GET /api/v1/stock/deletions feed. That legacy feed returns its own pollStartedAt; keep updated_since fixed while paging and advance it to the returned checkpoint only after every page succeeds. Its first full-history sync may require several pages. The Node and Python reference clients include syncStockDeletions and sync_stock_deletions helpers; store this checkpoint independently from the stock event feed. Periodically do a full stock refresh to reconcile deletions from before this feed was introduced. CRM has no hard-delete operation; use filter=all and sync status changes.
Third-party data and advertising
This is an API for a business’s own stock. It does not grant access to Auto Trader Connect data, BCA auction services, paid vehicle checks or direct advertising-platform publishing. Those services need their own provider agreements.
Stock reviews and owned follow-ups
Use a separate Stock reviews connection to read the review queue or record Keep price, Change asking price or Plan disposal. Review permissions include private recorded costs and evidence. A saved decision has an owner, a future review date and an original confirmation for safe retry. A price change prepares the Signal advert for a fresh review; publication uses the separate advert permission. Existing Stock, CRM, finance and deal keys receive no new review access.
Stock review setup and recovery guide · Node.js client with review helpers
The background scan creates one owned task when a recorded review is due. Recorded evidence keeps its qualification; these endpoints do not supply licensed market feeds. Asking-price decisions leave purchase/sale invoices and real payments separate.
Start with your stock
Connect Signal Trade to your system.
The workspace and Trade APIs have no Signal subscription fee. Motor-trade business approval is required.