> For the complete documentation index, see [llms.txt](https://docs.loopit.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.loopit.co/settings/integrations/dms-inventory-integration.md).

# DMS inventory integration

Loopit accepts a DMS inventory feed so vehicles stay in sync: create or update, remove from the feed, look up a VIN, and reconcile.

Loopit issues Auth0 credentials and an `account_id` per location. One set of credentials can cover several locations.

### **Authentication**

OAuth 2.0 client credentials.

1. Request an access token from the Auth0 token URL Loopit provides (`grant_type=client_credentials`, plus `client_id`, `client_secret`, and `audience`).
2. Send it on every request:

Authorization: Bearer {access\_token}

Content-Type: application/json

Token URL, client id, secret, audience, and base URL (for example <https://platform.api.loopit.co/>) come from Loopit at go-live.

### **Endpoints**

<table data-header-hidden><thead><tr><th width="159.3515625"></th><th></th><th></th></tr></thead><tbody><tr><td><strong>Method</strong></td><td><strong>Path</strong></td><td><strong>Purpose</strong></td></tr><tr><td><code>GET</code></td><td><code>/api/v2/auth/account-access</code></td><td>List locations this integration can write to</td></tr><tr><td><code>POST</code></td><td><code>/api/v2/inventory</code></td><td>Create or update a vehicle</td></tr><tr><td><code>DELETE</code></td><td><code>/api/v2/inventory</code></td><td>Remove a vehicle from the feed</td></tr><tr><td><code>GET</code></td><td><code>/api/v2/inventory/reconcile</code></td><td>Reconcile linked vehicles</td></tr><tr><td><code>POST</code></td><td><code>/api/v2/reports/subscription-pricing</code></td><td>VIN lookup (vehicle + plans)</td></tr></tbody></table>

### **Discover locations**

`GET /api/v2/auth/account-access`

Returns the `account_id` values this integration can write to. Use that id on inventory, reconcile, and VIN lookup calls. It is not the DMS site code.

***

### **Create or update a vehicle**

`POST /api/v2/inventory`

Creates the vehicle if Loopit has not seen `meta.itemId`. Updates it if it has. If the VIN already exists under a different `itemId`, the request fails unless `overwrite_existing_vin` is `true`.

#### **Required fields**

| **Field**                | **Notes**                                                         |
| ------------------------ | ----------------------------------------------------------------- |
| `vin`                    | 17-character VIN                                                  |
| `make`                   |                                                                   |
| `model`                  |                                                                   |
| `manuYear` or `compYear` | Four-digit year (`YYYY`)                                          |
| `stockNo`                | Max 20 characters                                                 |
| `meta.itemId`            | Stable DMS id. Use the same value on every update and on delete   |
| `meta.account_id`        | Loopit location id (integer)                                      |
| `meta.reconcileHash`     | Change this when the record changes so reconcile can detect drift |

#### **Used if sent**

| **Field**                 | **Maps to**                                                                                                  |
| ------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `nvic`                    | Stored on the vehicle. Used for Glass pricing when present                                                   |
| `baseColour`              | Colour                                                                                                       |
| `condition`               | `New`, `Demo`, or `Used` (default `Used`)                                                                    |
| `advertisedPrice`         | Retail price                                                                                                 |
| `odometer`                | Stored as acquisition odometer when greater than 0. `0` does not overwrite an existing reading               |
| `rego`                    | Licence plate                                                                                                |
| `badge`                   | Trim                                                                                                         |
| `transmissionDescription` | Transmission                                                                                                 |
| `fuelType`                | Fuel                                                                                                         |
| `seats`                   | Integer                                                                                                      |
| `doors`                   | Integer                                                                                                      |
| `body`                    | Body type                                                                                                    |
| `photoList`               | Photos, in `position` order. Only `photoUrl` is imported (`photoHash` / `photoType` are accepted and unused) |

#### **Optional Loopit fields**

| **Field**                | **Behaviour**                                                                          |
| ------------------------ | -------------------------------------------------------------------------------------- |
| `subscriptionPrice`      | Omit for auto weekly price. Number = weekly price including GST. `null` = skip pricing |
| `pricingProvider`        | `glass` (default)                                                                      |
| `overwrite_existing_vin` | Relink an existing VIN to this `itemId`                                                |
| `maintain_vehicle_state` | Keep the current bookable flag on update                                               |

Other Dealer Solutions-style fields (`comments`, `driveType`, `series`, `cylinders`, `bodyColour`, `driveawayPrice`, `location`, and similar) can be sent and are ignored.

#### **Example request**

```jsx
{
  "vin": "KMHDB51DLBU360526",
  "make": "Audi",
  "model": "A3",
  "manuYear": "2009",
  "stockNo": "U183415",
  "nvic": "ECQ09A",
  "condition": "Used",
  "baseColour": "Black",
  "rego": "112266",
  "odometer": "94568",
  "advertisedPrice": "24995",
  "badge": "SX",
  "body": "Hatchback",
  "fuelType": "Petrol - Unleaded",
  "transmissionDescription": "Automatic",
  "seats": 5,
  "doors": 5,
  "photoList": [
    {
      "position": "1",
      "photoUrl": "<https://example.com/photo-1.jpg>"
    }
  ],
  "meta": {
    "itemId": "25723977",
    "account_id": 53,
    "reconcileHash": "a3f13fd684bcd17c51e80ea4cdd4a8e2"
  }
}
```

#### **Success response**

```jsx
{
  "data": {
    "itemId": "25723977",
    "account_id": 53,
    "reconcileHash": "a3f13fd684bcd17c51e80ea4cdd4a8e2",
    "stockNo": "U183415",
    "notes": "",
    "vehicle_id": "…"
  }
}
```

***

### **Remove a vehicle from the feed**

`DELETE /api/v2/inventory?account_id=53&itemId=25723977`

Unlinks the DMS id and takes the vehicle off the bookable fleet. It is not permanently deleted. It will not show in reconcile. Posting the same VIN later with `overwrite_existing_vin: true` relinks it.

`itemId` and `account_id` are required.

***

### **Reconcile**

`GET /api/v2/inventory/reconcile?account_id=53`

Returns vehicles still linked to this location.

| **Query**       | **Behaviour**                                                                                                                    |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `account_id`    | Required                                                                                                                         |
| `reconcileHash` | Optional. Limit to that hash                                                                                                     |
| `state`         | Optional. `available`, `reserved`, `subscribed`, `unavailable`, `archived`, or `all`. Default omits `unavailable` and `archived` |

#### **Example response**

```jsx
{
  "data": [
    {
      "itemId": "25723977",
      "stockNo": "U183415",
      "account_id": 53,
      "reconcileHash": "a3f13fd684bcd17c51e80ea4cdd4a8e2",
      "notes": null,
      "state": "available",
      "substate": "available",
      "subscribeable": true
    }
  ]
}
```

| **`state`**   | **Meaning**                           |
| ------------- | ------------------------------------- |
| `available`   | Bookable, no live or reserved booking |
| `reserved`    | Confirmed or requested booking        |
| `subscribed`  | Active or paused booking              |
| `unavailable` | Not bookable                          |
| `archived`    | Archived in Loopit                    |

***

### **VIN lookup (subscription pricing)**

`POST /api/v2/reports/subscription-pricing`

Returns the vehicle for that VIN at this location, plus the plans a customer can sign up to. Use it after inventory sync when you need current price, availability, and photos.

Same auth as inventory. Send `account_id` and `vin` in the JSON body.

#### **Request**

```jsx
{
  "account_id": 53,
  "vin": "KMHDB51DLBU360526"
}
```

| **Field**    | **Required** | **Notes**                            |
| ------------ | ------------ | ------------------------------------ |
| `account_id` | Yes          | Loopit location id                   |
| `vin`        | Yes          | Must already exist in that workspace |

#### **Success response**

```jsx
{
  "success": true,
  "message": "Report generated",
  "data": {
    "vehicle": {
      "stock_number": "U183415",
      "identification_number": "KMHDB51DLBU360526",
      "nvic": "ECQ09A",
      "year": 2009,
      "make": "Audi",
      "model": "A3",
      "badge": "SX",
      "registration_number": "112266",
      "account_id": 53,
      "retail_price": 24995,
      "vehicle_state": "active",
      "state": "available",
      "substate": "available",
      "seats": 5,
      "doors": "5 door",
      "body_style": "Hatchback",
      "condition": "Used",
      "fuel_type": "Petrol - Unleaded",
      "transmission": "Automatic",
      "odometer": 94568,
      "colour": "Black",
      "available": 1,
      "subscription_price_per_week": 199.0,
      "subscribeable": true,
      "images": ["<https://example.com/photo-1.jpg>"],
      "adjustments": [
        {
          "plan_id": "…",
          "subscription_price": 199.0,
          "plan_price": 50.0,
          "total_price": 249.0
        }
      ]
    },
    "plans": [
      {
        "id": "…",
        "account_id": 53,
        "name": "Essential",
        "description": null,
        "price": 50,
        "billing_frequency": "weekly",
        "establishment_fee": 500,
        "minimum_term": 30,
        "distance_allowance": 500,
        "fee_per_additional_km": 0.33,
        "plan_state": "active",
        "version": 1
      }
    ],
    "account": {
      "id": 53,
      "name": "Acme Rentals"
    },
    "equipment": [],
    "variant": {
      "power": null,
      "fuel_combined": null,
      "engine_size": null
    }
  }
}
```

#### **How to read it**

* `vehicle.state` matches reconcile: `available`, `reserved`, `subscribed`, `unavailable`.
* `subscribeable` is `true` only when `state` is `available`.
* `vehicle_state` is `active` if the vehicle is bookable, otherwise `archived`.
* `substate` is `deleted` when the vehicle is not bookable; otherwise it matches `state`.
* Weekly prices are **including GST**.
* `vehicle.adjustments` is the split per plan: vehicle weekly (`subscription_price`) + plan extras (`plan_price`) = `total_price`.
* `plans` is the same feed without those three price fields on the plan object. Only packages that allow customer signup are included.
* `equipment` and `variant` are reserved. They currently return empty / null.

#### **VIN lookup errors**

| **Status**                 | **When**                                        |
| -------------------------- | ----------------------------------------------- |
| `400`                      | `account_id` missing                            |
| `401`                      | Invalid or expired token                        |
| `422`                      | `vin` missing                                   |
| `200` + `"success": false` | VIN not in this workspace (`Vehicle not found`) |
| `500`                      | Unexpected failure                              |

Unknown VIN returns **HTTP 200** with `"success": false` and `"message": "Vehicle not found"`. Check `success`, not only the status code.

***

### **Errors**

| **Status** | **When**                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------ |
| `400`      | `account_id` missing                                                                       |
| `401`      | Invalid or expired token                                                                   |
| `404`      | Unknown `account_id`, or `itemId` not found on delete                                      |
| `422`      | Validation failed, VIN already linked to another `itemId`, or pricing could not be applied |
| `500`      | Unexpected failure                                                                         |

VIN conflict example: *VIN already exists with item ID: {id}. Use that item ID to keep in sync, or set overwrite\_existing\_vin to true.*

***

### **Implementation notes**

* Treat `meta.itemId` as the primary key. Do not rotate it for the same physical vehicle.
* Bump `reconcileHash` when any synced field changes.
* Send one request per vehicle per location (`account_id`).
* Photos must be reachable over HTTPS. Loopit imports them asynchronously.
* NVIC is optional. Send it when you have it so Glass pricing is more accurate.
* Loopit can share a Postman collection and sandbox credentials at onboarding. Do not rely on older public Postman docs for this API.
