[← API v2 Overview](https://motordesk.com/api-docs/v2/)

# Changelog

Notable changes to the API, newest first. Categories are: Added, Changed, Deprecated, Removed, Fixed, Security. See [Versioning & Deprecation](https://motordesk.com/api-docs/v2/versioning/) for what counts as a breaking change.

## 2026-09-26 · v2.0

- Added Leads return rating (the AI's buying-intent rating, 1-5, or null when not rated) and summary (a short description of where the conversation stands, or null) in both views, and ai in the full view: intent (one of the ids used by type, or other), rating_reason, next_action, junk, junk_confidence and processed. GET /leads accepts rating_min (1-5) to list only leads rated at or above it. The rating measures buying intent, not spam: junk is still reported through status. lead.updated fires each time the AI updates a lead, usually once per inbound customer message.

## 2026-09-03 · v2.0

- Added POST /invoices/{id}/finance-invoice generates the finance provider's draft invoice for a sale that was issued without one. The sale must be issued or paid, carry a finance provider and a finance amount, not be fully credited and not already have a finance provider invoice. The lender's draft is built from the sale's own data and returned, and the sale is marked part funded by the provider. Finance provider invoices are a MotorDesk-only document for the finance company and are not sent to accounting software. Issuing a draft with finance.generate set still creates it automatically.
- Added Appraisals reach the API through the trade-in. part_exchange[].appraisal_id on POST and PATCH /invoices and /orders links a part-exchange row to your appraisal of the customer's vehicle (it must be an appraisal for the same registration as the trade-in): when that vehicle is added to stock it inherits the appraisal's condition, preparation tasks and media, and the appraisal is marked Accepted at the row's value. GET /vehicles accepts appraisal_contact to list a contact's appraisals (pair with type=appraisal). Webhook events appraisal.offered, appraisal.accepted, appraisal.declined and appraisal.reopened carry the appraisal as a vehicle resource (a new appraisal fires the existing vehicle.created, with type "appraisal").
- Added Invoices, orders and purchases are versioned. Each returns version (1, 2 or 3), stamped at creation and never changed; those created from this release are version 3. Version 1 (created before per-line discounts) keeps its invoice-level discounts[] and adjustments[] on PATCH. From version 2, a discount lives on the line and discounts[] returns 422; version 3 has no adjustments[] either (422) - an admin fee or delivery charge is a line, and outstanding finance is a part_exchange settlement.
- Added Vehicle finance settlements on part exchanges. POST and PATCH /invoices and /orders accept part_exchange[].settlement as {amount, finance_company (the lender's contact id), reference, valid_until}; the trade-in value stays the full allowance and the settlement amount is added back to what the customer pays (totals.settlement, no tax). A settlement requires purchase = Yes and a part exchange with a positive value that reduces the sale total, because it is paid against the part-exchange purchase invoice: that purchase carries it on its acquisition line, receives the net allowance as its Part Exchange payment when the sale is paid, and stays issued until the settlement is paid to the lender (GET /purchases/{id} exposes settlement_outstanding). Outstanding finance is never a line field - lines[].settlement returns 422 pointing here.
- Added POST /purchases/{id}/payments accepts payee (a contact id) to record who a payment went to when not the supplier, and the payment method "Finance Settlement"; payments[] on GET carry payee. A payee naming the purchase's own supplier is normalised away (stored and returned as payee: null) - null already means the payment went to the supplier - unless that supplier is also a lender the purchase owes a settlement to, where the payee is kept because it is what marks the payment as the settlement.
- Added GET /purchases accepts settlement_outstanding=true to list only purchases whose expected finance settlement has not yet been fully recorded as paid to the lender. The paid side reads the payment projection (payments recorded with the "Finance Settlement" method, or to a named payee); each document's own settlement_outstanding field remains the authoritative figure.
- Added Per-line discounts on invoices, orders and purchases. POST and PATCH accept lines[].discount as {value, type}, where type is currency (the default) or percent of the line's pre-discount product total; the discount inherits the line's tax treatment and accounting code, and a percentage cannot exceed 100. Every line returns discount as {value, type, amount} (the resolved deduction) or null when the line carries none.
- Added Per-line vehicle attribution on purchases. POST and PATCH /purchases accept lines[].vehicle (an id, or an object with id or tag) to attribute an individual line's cost to that vehicle's additional costs - each attributed vehicle receives its own cost row. additional_cost_vehicle is the default for lines without their own vehicle, and purchases with no per-line attribution behave exactly as before. Purchase lines return vehicle (the attributed vehicle id, or null). Only valid on purchases (422 validation_failed elsewhere) and only for in-stock or customer vehicles that have not been deleted.
- Security The appraisal block returned for appraisal-type vehicles (GET /vehicles and the appraisal webhook payloads) is now a documented data.appraisal section, and no longer includes confirm.max_offer - the dealer's internal negotiation ceiling previously reached API output through the untyped pass-through. Appraisals stay read-only through the API: they are written through the dashboard workflow, or linked from a document via part_exchange[].appraisal_id.

## 2026-08-25 · v2.0

- Added POST /leads accepts an optional vehicle (stock id) or vehicle_tag, associating the vehicle of interest with the new lead in the same call - the equivalent of following up with POST /leads/{id}/vehicles. Create only; the association on an existing lead is still managed through the lead vehicles endpoint.
- Added POST /appointments accepts an optional vehicle (stock id) or vehicle_tag, linking the vehicle of interest to the new appointment - so a customer-booked test drive names its car and is queryable via the vehicle filter on GET /appointments. A lead is not required; when the appointment has one, the vehicle also joins that lead's vehicles of interest. Appointments now return vehicle (the associated vehicle id, or null) so the link reads back, and PATCH rejects the field rather than silently ignoring it - the association on an existing appointment is managed through POST /leads/{id}/vehicles.
- Changed POST /appointments and POST /leads/{id}/appointments now validate staff assignment: every id in user must belong to a staff user in the business (see GET /reference/staff-users); unknown ids return 422 instead of creating an appointment assigned to nobody. The OpenAPI booking example now shows a real booking type id (vehicle_test_drive), and the new AppointmentCreateInput schema declares date, time, purpose and user in required[].
- Fixed Appointments on the Primary Calendar now read back calendar: "Primary Calendar" (previously an empty string), matching GET /appointments/calendars, and the calendar filter on GET /appointments accepts "Primary Calendar". Other calendar names round-trip as before.
- Changed The status filter on GET /leads accepts the status names leads read back (new, new_message, read, replied, closed, junk) as well as the codes 0-5, and both are now documented on the parameter. The GET /vehicles status filter documentation now explains that sold (3) includes handed-over vehicles that read back as complete, and for-sale (2) includes reserved.
- Changed POST /vehicles now returns 422 for an empty body instead of creating a blank draft: provide a registration or identifier to create from lookup, or vehicle data and/or media to create directly.
- Fixed The GET /listings filters (make, model, fuel, transmission, body, colour, year, mileage, price ranges, and updated_since/updated_before for delta sync) are now documented in the OpenAPI specification - they were always available but previously undocumented.
- Added GET /business returns the authenticated business's own profile: name, legal name, company and VAT numbers, contact details, address, social handles, localisation (currency, currency symbol, timezone, distance units and date formats), the name it gives its sales tax, and its opening hours. Opening hours cover the standard set plus the booking, delivery and collection overrides, the weekdays that are by appointment only, and irregular dates such as bank holidays. It also reports consumer finance as finance.enabled (the business offers finance - its finance option is on and a provider is connected) and finance.fca_number (the FCA firm reference number it is authorised under). Requires the new business:read scope; the endpoint is read-only and returns profile information only - finance provider settings and credentials are never returned.
- Added GET /appointments/availability returns the bookable dates and times for each enabled booking type, from the same engine the dealer's own booking pages use - opening hours, appointment duration, per-hour and per-day limits, minimum notice, existing bookings and holiday closures are all applied. Pass ?booking=<type> for one type or ?date=YYYY-MM-DD for one date. Slot times are returned as HH:MM and can be sent straight back as the time on POST /appointments. Capacity is shared per calendar, so booking types on the same calendar compete for the same slots. Availability is advisory: creating an appointment does not require a free slot, matching the dashboard.
- Added GET /reference/booking-types now includes duration_minutes for each booking type, so the appointment length is available alongside the id, name and calendar. For the dates and times a type can actually be booked, use GET /appointments/availability.

## 2026-08-20 · v2.0

- Added Vehicle additional-cost purchases: POST and PATCH /purchases accept additional_cost_vehicle ({id} or {tag}) to link the whole purchase to a vehicle as an additional cost, matching the dashboard's "Create Purchase Invoice" flow - the document total feeds the vehicle's Purchase & Costs and its profit, and cancelling or deleting the draft removes it again. Set null to unlink or a different vehicle to re-target (both API-only, drafts only). The purchase resource returns additional_cost_vehicle (null when not linked). A vehicle holds at most 50 cost rows; an attach beyond that returns 422 vehicle_costs_full.
- Added Item-linked credit notes on invoices, orders and purchases. Document line items now expose a stable key (lines[].key, and options[].key combined as "<line key>-o-<option key>") and credit note lines return a source field attributing them to the line they reverse. POST /{type}/{id}/credit-notes accepts an optional source on each line: the credit is validated against that invoice line, capped at what remains creditable on it (409 credit_cap_exceeded when exceeded), and inherits the line's own tax treatment. All additions are backwards-compatible; free-form credit lines are unchanged and return source: null.
- Changed PATCH /invoices/{id}, /orders/{id} and /purchases/{id} no longer clear the document's lines (or sold vehicle) when the body omits them: a body carrying neither lines nor vehicle now leaves the existing line collection untouched, so a purchase's additional_cost_vehicle link can be changed - or removed with null - by a link-only body. Send lines: [] to clear the lines explicitly. Bodies that do carry lines and/or vehicle behave exactly as before: the collection is rebuilt from what is supplied.
- Changed PATCH /purchases/{id} against a purchase linked to a vehicle ACQUISITION (created from the dashboard's vehicle purchase flow) now returns 409 purchase_acquisition_linked instead of silently destroying the acquisition lines. Acquisition purchases remain manageable from the dashboard.
- Changed POST /appointments now requires purpose and user in addition to date and time. Creating an appointment without a purpose, or without at least one assigned staff user (user as a single id or a non-empty array of ids), returns 422 validation_failed, matching the dashboard appointment form. PATCH /appointments/{id} is unchanged - a partial update may still omit any field.

## 2026-08-04 · v2.0

- Removed The brand_reserve and brand_sold options on PATCH /vehicles/{id}/media/{key} have been removed; sending either now returns a 422 validation_failed. Reserved and sold overlays are not per-image state: they are derived from the vehicle's own reserved/sold status and applied automatically to its first photo. Both still appear in the media response so you can see which photo currently carries a badge. Use POST/DELETE /vehicles/{id}/reserve and the sell endpoints to change the underlying state.

## 2026-06-22 · v2.0

- Added Initial release of the MotorDesk API v2: vehicles and stock, contacts, leads, appointments, calls, invoices, orders, purchases, documents, deals, blog articles, reviews and webhooks. JSON over HTTPS with per-business scoped bearer tokens, a complete OpenAPI 3.1 specification, and an official PHP SDK. The API is in beta; breaking changes remain possible until the stable release on 1 January 2027.
