# Collection runs

Newest first. Specials runs carry the never-blank outcome, OCR and LLM counts.

## Endpoint

```
GET https://api.motorsdynamic.com/api/v2/runs
```

## Plan and price

| Plan | Access | Price |
| --- | --- | --- |
| Free | No | not included |
| Growth | No | not included |
| Scale | No | not included |
| 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 |
| --- | --- | --- | --- | --- |
| `domain_id` | query | integer | yes |  |
| `kind` | query | `inventory` or `specials` | 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/runs?domain_id=201&kind=specials" \
  -H "x-api-key: $MD_API_KEY"
```

JavaScript:

```javascript
const res = await fetch("https://api.motorsdynamic.com/api/v2/runs?domain_id=201&kind=specials", {
  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/runs?domain_id=201&kind=specials",
    headers={"x-api-key": os.environ["MD_API_KEY"]},
    timeout=10,
)
res.raise_for_status()
print(res.json())
```

## Example response

```json
{
  "data": [
    {
      "run_id": 77,
      "domain_id": 201,
      "kind": "specials",
      "state": "complete",
      "mode": "publish",
      "started_at": "2026-09-24T10:00:00.000000Z",
      "finished_at": "2026-09-24T10:00:00.000000Z",
      "adapter_version": "specials-generic-1",
      "fetch_tier": "direct",
      "pages_fetched": 3,
      "requests": 4,
      "counts": {
        "added": 1,
        "changed": 1,
        "unchanged": 5,
        "removed": 0
      },
      "error": null,
      "specials": {
        "outcome": "RECIPE_HIT",
        "reason": "7 validated via proven recipe(s)",
        "cms": "teamvelocity",
        "observations": 7,
        "listed": 1,
        "changed": 1,
        "relisted": 0,
        "unchanged": 5,
        "removed": 0,
        "renders": 0,
        "ocr_read": 0,
        "ocr_skipped": {},
        "llm_calls": 0,
        "llm_capped": false,
        "strategies": {
          "tv_api": 7
        }
      }
    }
  ],
  "page": {
    "limit": 50,
    "has_more": false,
    "next_cursor": null
  },
  "meta": {}
}
```

## Response fields

| Field | Type | Description |
| --- | --- | --- |
| `data[]` | array of object |  |
| `data[].run_id` | integer |  |
| `data[].domain_id` | integer |  |
| `data[].kind` | `inventory` or `specials` |  |
| `data[].state` | `claimed` or `running` or `complete` or `incomplete` or `failed` or `expired` |  |
| `data[].mode` | `shadow` or `publish` |  |
| `data[].started_at` | string or null | RFC 3339 UTC with microseconds. |
| `data[].finished_at` | string or null | RFC 3339 UTC with microseconds. |
| `data[].adapter_version` | string or null |  |
| `data[].fetch_tier` | string or null |  |
| `data[].pages_fetched` | integer or null |  |
| `data[].requests` | integer or null |  |
| `data[].counts` | object |  |
| `data[].counts.added` | integer |  |
| `data[].counts.changed` | integer |  |
| `data[].counts.unchanged` | integer |  |
| `data[].counts.removed` | integer |  |
| `data[].error` | object or null |  |
| `data[].error.class` | string |  |
| `data[].error.message` | string or null |  |
| `data[].specials` | object or null |  |
| `data[].specials.outcome` | `RECIPE_HIT` or `DISCOVERY_HIT` or `SWEEP_INCOMPLETE` or `RECIPE_MISS` or `CONFIRMED_NONE` |  |
| `data[].specials.reason` | string or null |  |
| `data[].specials.cms` | string or null |  |
| `data[].specials.observations` | integer |  |
| `data[].specials.listed` | integer |  |
| `data[].specials.changed` | integer |  |
| `data[].specials.relisted` | integer |  |
| `data[].specials.unchanged` | integer |  |
| `data[].specials.removed` | integer |  |
| `data[].specials.renders` | integer |  |
| `data[].specials.ocr_read` | integer |  |
| `data[].specials.ocr_skipped` | object |  |
| `data[].specials.llm_calls` | integer |  |
| `data[].specials.llm_capped` | boolean |  |
| `data[].specials.strategies` | object |  |
| `page` | object |  |
| `page.limit` | integer |  |
| `page.has_more` | boolean |  |
| `page.next_cursor` | string or null |  |
| `meta` | 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. |
| 402 | `plan_required` | The endpoint is on a higher plan. The body names the plan and links to the upgrade. |
| 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).
