# Vehicle history

Every event recorded for a VIN, oldest first: the first listing, each price change, status changes and removal, with the dealer site it was seen on. `meta.sources` reports the status of each history source. A page with a failing source has `meta.partial: true` and no `next_cursor`: retry it.

## Endpoint

```
GET https://api.motorsdynamic.com/api/v2/vehicles/{vin}/timeline
```

## Plan and price

| Plan | Access | Price |
| --- | --- | --- |
| Free | Yes | included in the 1,000 monthly calls |
| Growth | Yes | $0.0015 per call |
| Scale | Yes | $0.0009 per call |
| Enterprise | Yes | contract |

A call is billed when it answers 2xx. See [pricing](https://developers.motorsdynamic.com/pricing.md).

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `vin` | path | string | yes |  |
| `sources` | query | string | no | Comma-separated subset of v2, md_legacy, foureyes. |
| `since` | query | string (date-time) | no |  |
| `until` | query | string (date-time) | no |  |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | `page.next_cursor` from the previous page, unchanged. |

## Example request

curl:

```bash
curl "https://api.motorsdynamic.com/api/v2/vehicles/1FTFW1E50NFA00001/timeline?sources=v2%2Cmd_legacy&limit=2" \
  -H "x-api-key: $MD_API_KEY"
```

JavaScript:

```javascript
const res = await fetch("https://api.motorsdynamic.com/api/v2/vehicles/1FTFW1E50NFA00001/timeline?sources=v2%2Cmd_legacy&limit=2", {
  headers: { "x-api-key": process.env.MD_API_KEY },
});
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`);
const data = await res.json();
console.log(data);
```

Python:

```python
import os, requests

res = requests.get(
    "https://api.motorsdynamic.com/api/v2/vehicles/1FTFW1E50NFA00001/timeline?sources=v2%2Cmd_legacy&limit=2",
    headers={"x-api-key": os.environ["MD_API_KEY"]},
    timeout=10,
)
res.raise_for_status()
print(res.json())
```

## Example response

```json
{
  "data": [
    {
      "event_id": "10000000-0000-5000-8000-000000000001",
      "domain_id": 101,
      "vin": "1FTFW1E50NFA00001",
      "source_key": "1FTFW1E50NFA00001",
      "event_type": "listed",
      "observed_at": "2026-06-01T12:00:00.500000Z",
      "prev": null,
      "next": {
        "price": 45000
      },
      "entity_version": null,
      "episode": null,
      "freshness": {
        "observed_at": "2026-06-01T12:00:00.500000Z"
      },
      "provenance": {
        "source": "md_legacy",
        "adapter_version": "md_legacy-2026-09-24",
        "run_id": null,
        "quality": "ok"
      }
    },
    {
      "event_id": "00000000-0000-4000-8000-000000000002",
      "domain_id": 101,
      "vin": "1FTFW1E50NFA00001",
      "source_key": "1FTFW1E50NFA00001",
      "event_type": "price_changed",
      "observed_at": "2026-09-21T00:00:00.000000Z",
      "prev": {
        "price": 41000
      },
      "next": {
        "price": 40000
      },
      "entity_version": 3,
      "episode": 1,
      "freshness": {
        "observed_at": "2026-09-21T00:00:00.000000Z"
      },
      "provenance": {
        "source": "v2",
        "adapter_version": "dealer-inspire@3",
        "run_id": 77,
        "quality": null
      }
    }
  ],
  "page": {
    "limit": 2,
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "vin": "1FTFW1E50NFA00001",
    "sources": {
      "v2": "ok",
      "md_legacy": "ok"
    },
    "partial": false
  }
}
```

## Response fields

| Field | Type | Description |
| --- | --- | --- |
| `data[]` | array of object |  |
| `data[].event_id` | string (uuid) |  |
| `data[].domain_id` | integer or null |  |
| `data[].vin` | string or null |  |
| `data[].source_key` | string or null |  |
| `data[].event_type` | `listed` or `price_changed` or `price_basis_changed` or `mileage_changed` or `photos_changed` or `status_changed` or `correction` |  |
| `data[].observed_at` | string |  |
| `data[].prev` | object | State before the event (object or null). |
| `data[].next` | object | State after the event (object or null). |
| `data[].entity_version` | integer or null |  |
| `data[].episode` | integer or null |  |
| `data[].freshness` | object |  |
| `data[].freshness.observed_at` | string |  |
| `data[].provenance` | object |  |
| `data[].provenance.source` | `v2` or `md_legacy` or `foureyes` |  |
| `data[].provenance.adapter_version` | string or null |  |
| `data[].provenance.run_id` | integer or null |  |
| `data[].provenance.quality` | string or null | Comma list of archive caveats, for example mass_removal_hour. |
| `page` | object |  |
| `page.limit` | integer |  |
| `page.has_more` | boolean |  |
| `page.next_cursor` | string or null |  |
| `meta` | object |  |
| `meta.vin` | string |  |
| `meta.partial` | boolean |  |
| `meta.sources` | object |  |

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_parameter` | A parameter is missing or malformed. The message names it. |
| 401 | `invalid_api_key` | The key is missing, revoked or mistyped. |
| 403 | `not_entitled` | The key may not read this dealer or source. |
| 429 | `rate_limited or quota_exceeded` | Slow down (see `Retry-After`), or the Free monthly calls are used up. |
| 504 | `query_timeout` | The query passed its 4 second budget. Narrow the filters. Not billed. |

Every error has the same body: `{ "error": "<code>", "message": "..." }`. Full list: [errors](https://developers.motorsdynamic.com/docs/errors.md).
