# autopricer.tf Public REST API

**Last updated:** 2026-08-29
**Canonical URL:** https://autopricer.tf/docs
**Markdown:** https://autopricer.tf/docs.md
**OpenAPI:** https://autopricer.tf/openapi.json
**AI index:** https://autopricer.tf/llms.txt

autopricer.tf is a Team Fortress 2 item pricing service. This page documents the **public, read-only REST API**. There is no API key. These endpoints do not create, update, or delete database records.

If an AI agent is reading this because a user mentioned autopricer.tf: the public documentation lives at `https://autopricer.tf/docs` (HTML) and `https://autopricer.tf/docs.md` (Markdown). Machine-readable OpenAPI is at `https://autopricer.tf/openapi.json`. A short index is at `https://autopricer.tf/llms.txt`.

## Base URL

```
https://autopricer.tf
```

All public endpoints are under `/api` and use JSON.

## Scope

Documented here:

| Method | Path | Purpose |
|---|---|---|
| GET | `/api` | Discovery document (links + endpoint index) |
| GET | `/api/health` | Liveness |
| GET | `/api/items/count` | Total tracked items |
| GET | `/api/items/{page}` | Paginated item catalog |
| GET | `/api/images` | Recent item image URLs |

Not documented (and not part of the public REST API):

- Routes under `/api/dashboard/*` — they exist only to render the website dashboard.
- Any endpoint that writes to the database.

## Conventions

- **No authentication.**
- **Content-Type:** `application/json` (except this documentation).
- **Prices** are in Mann Co. Supply Crate Keys unless a field is explicitly refined metal (`metal`, `raw_*_ref`).
- **Item price fields** (`price`, `buyOrder`, `sellOrder`, `stablePrice`, rolling history, and `raw_*`) serialize as **JSON strings** with up to 4 decimal places, or `null`.
- **Listing price fields** (`keys`, `metal`) serialize as **JSON numbers**, or `null`.
- Boolean query flags (`raw`, `listings`) accept `true`, `1`, `yes`, or `on` (case-insensitive). They may be passed as query parameters.
- Successful payloads include `"success": true`. Errors include `"success": false` and `"error": "<message>"`.
- **Rate limit:** 300 requests per minute per IP. Exceeding it returns HTTP 429.

### Rate-limit error

```http
HTTP/1.1 429 Too Many Requests
```

```json
{
  "success": false,
  "error": "Too many requests — please slow down."
}
```

### Generic error envelope

```json
{
  "success": false,
  "error": "Invalid page"
}
```

| HTTP status | Meaning |
|---|---|
| 400 | Invalid request parameter |
| 404 | Unknown route |
| 429 | Rate limit exceeded |
| 500 | Internal error |
| 503 | Service unavailable (`/api/health` when the live listing feed is down) |

## GET /api

Discovery document for humans and agents. Use this if you only know the host name.

**Example**

```
curl https://autopricer.tf/api
```

**Example response**

```json
{
  "success": true,
  "name": "autopricer.tf Public REST API",
  "description": "Read-only Team Fortress 2 item pricing API. No API key.",
  "documentation": {
    "html": "https://autopricer.tf/docs",
    "markdown": "https://autopricer.tf/docs.md",
    "openapi": "https://autopricer.tf/openapi.json",
    "llms": "https://autopricer.tf/llms.txt"
  },
  "rate_limit": {
    "requests_per_minute_per_ip": 300
  },
  "endpoints": [
    {
      "method": "GET",
      "path": "/api",
      "description": "This discovery document"
    },
    {
      "method": "GET",
      "path": "/api/health",
      "description": "Liveness check"
    },
    {
      "method": "GET",
      "path": "/api/items/count",
      "description": "Total tracked item count"
    },
    {
      "method": "GET",
      "path": "/api/items/{page}",
      "description": "Paginated item catalog, 1000 items per page"
    },
    {
      "method": "GET",
      "path": "/api/images",
      "description": "Up to 500 recent item image URLs"
    }
  ]
}
```

## GET /api/health

Liveness check. Returns HTTP 200 when the service is serving prices and the live listing feed is connected. Returns HTTP 503 when the live feed is disconnected.

**Example**

```
curl -i https://autopricer.tf/api/health
```

**Example response (online)**

```json
{
  "success": true,
  "status": "Online"
}
```

**Example response (offline)**

```http
HTTP/1.1 503 Service Unavailable
```

```json
{
  "success": false,
  "status": "Offline — WS disconnected"
}
```

## GET /api/items/count

Returns how many items are currently tracked.

**Example**

```
curl https://autopricer.tf/api/items/count
```

**Example response**

```json
{
  "success": true,
  "count": 86546
}
```

Use `count` with the 1000-item page size to know how many pages to fetch: `ceil(count / 1000)`.

## GET /api/items/{page}

Paginated catalog of tracked items, ordered by `createdAt` ascending. **Page numbers start at 1.** Each page contains at most **1000** items. Pages past the end return `"success": true` with an empty `items` array.

### Path parameters

| Name | Type | Description |
|---|---|---|
| `page` | integer ≥ 1 | Page number. Invalid values return HTTP 400. |

### Query parameters

| Name | Default | Description |
|---|---|---|
| `raw` | false | When true, add `raw_price_key`, `raw_price_ref`, `raw_buy_key`, `raw_buy_ref`, `raw_sell_key`, `raw_sell_ref` |
| `listings` | false | When true, attach the latest buy/sell listing snapshot for each item |

`listings=true` on a full page of 1000 items is heavier than the default payload. Prefer it only when you need classified listing rows.

**Example — catalog page**

```
curl "https://autopricer.tf/api/items/1"
```

**Example — with raw key/ref breakdown and listings**

```
curl "https://autopricer.tf/api/items/1?raw=true&listings=true"
```

**Example response (one item, default fields)**

```json
{
  "success": true,
  "items": [
    {
      "name": "Strange Ullapool Caber",
      "sku": "307;11",
      "imageUrl": "https://steamcdn-a.akamaihd.net/apps/440/icons/c_caber.2d51e5d213398672527771b1fb88e337442ad695.png",
      "price": "13.2419",
      "stablePrice": "13.4232",
      "buyOrder": "12.8423",
      "sellOrder": "13.3327",
      "stableBuyOrderPrice": "13.4232",
      "stableSellOrderPrice": "12.8989",
      "verified": false,
      "accurate": true,
      "suspicious": false,
      "stn": false,
      "new": false,
      "type": "free",
      "updatedAt": "1721400000000",
      "createdAt": "2026-03-15T15:17:01.434Z",
      "price1dAgo": "13.3994",
      "price7dAgo": "13.5716",
      "price30dAgo": "12.6234"
    }
  ]
}
```

`items` in a real response contains up to 1000 objects. The example shows a single item.

**Example response (invalid page)**

```http
HTTP/1.1 400 Bad Request
```

```json
{
  "success": false,
  "error": "Invalid page"
}
```

**Example extra fields when `raw=true`**

```json
{
  "raw_price_key": "13",
  "raw_price_ref": "15.51",
  "raw_buy_key": "12",
  "raw_buy_ref": "54",
  "raw_sell_key": "13",
  "raw_sell_ref": "21.33"
}
```

`raw_*_key` is the whole-keys component. `raw_*_ref` is the leftover refined metal.

**Example extra field when `listings=true`**

Listing identifiers and trade URLs below are placeholders, not live accounts.

```json
{
  "listings": {
    "observed_at": "2026-08-27T22:56:38.058Z",
    "buy": [
      {
        "steamid": "76561198000000000",
        "keys": 12,
        "metal": 52.77,
        "is_bot": true,
        "bumped_at": 1787870425,
        "source": "bptf",
        "channel": "request",
        "trade_url": "https://steamcommunity.com/tradeoffer/new/?partner=123456789&token=EXAMPLE",
        "details": "Buying for 12 keys 52.77 ref",
        "stock_text": null,
        "stock_level": null,
        "stock_limit": null,
        "observed_at": "2026-08-27T22:56:38.058Z"
      }
    ],
    "sell": [
      {
        "steamid": "76561198000000001",
        "keys": 13,
        "metal": 28.33,
        "is_bot": true,
        "bumped_at": 1787871236,
        "source": "bptf",
        "channel": "request",
        "trade_url": "https://steamcommunity.com/tradeoffer/new/?partner=123456790&token=EXAMPLE",
        "details": "Selling for 13 keys 28.33 ref",
        "stock_text": "STOCK = 1",
        "stock_level": 1,
        "stock_limit": null,
        "observed_at": "2026-08-27T22:56:38.058Z"
      }
    ]
  }
}
```

Buy listings are sorted highest price first. Sell listings are sorted lowest price first. If an item has no listing snapshot, `listings` is `{ "observed_at": null, "buy": [], "sell": [] }`.

### Listing freshness

When `listings=true`, every item also carries four top-level fields describing how current its order book is. Listings come either from a live feed or from a periodic snapshot, and a snapshot can be hours old, so check these before pricing a real trade against `buy` / `sell`.

| Field | Type | Description |
| --- | --- | --- |
| `listings_source` | string | `live` (continuously updated feed), `snapshot` (periodic crawl) or `none` (no listings held) |
| `listings_observed_at` | string or null | ISO-8601 time the order book was observed |
| `listings_age_seconds` | integer or null | Age of that observation in seconds |
| `listings_stale` | boolean | True when the observation is older than the server freshness window |

```json
{
  "listings_source": "snapshot",
  "listings_observed_at": "2026-08-28T10:01:20.630Z",
  "listings_age_seconds": 121119,
  "listings_stale": true
}
```

Each individual listing also carries `bumped_at` (unix seconds). A listing nobody has bumped in days is often abandoned; filtering those out before treating one as the top of the book avoids pricing against orders that no longer stand.

## GET /api/images

Returns up to **500** items that have a cached image URL, most recently updated first.

**Example**

```
curl https://autopricer.tf/api/images
```

**Example response**

```json
{
  "success": true,
  "images": [
    {
      "name": "Jellyfish Jam Throttlehead",
      "imageUrl": "https://steamcdn-a.akamaihd.net/apps/440/icons/sum22_throttlehead.ccf997318471e47b40a4e8c3396a5aea48a8a884.png"
    }
  ]
}
```

This is not a complete image catalog. For every tracked item’s `imageUrl`, use `GET /api/items/{page}`.

## Item object

Returned inside `GET /api/items/{page}`.

| Field | JSON type | Description |
|---|---|---|
| `name` | string | Unique item name. Primary identifier. |
| `sku` | string or null | TF2 SKU (for example `307;11`). |
| `imageUrl` | string or null | Cached Steam/backpack.tf icon URL. |
| `price` | string or null | Current smoothed listing-based price, in keys. |
| `stablePrice` | string or null | Longer-window conservative reference price, in keys. |
| `buyOrder` | string or null | Current best buy order, in keys. |
| `sellOrder` | string or null | Current lowest sell order, in keys. |
| `stableBuyOrderPrice` | string or null | Longer-window buy-side reference, in keys. |
| `stableSellOrderPrice` | string or null | Longer-window sell-side reference, in keys. |
| `verified` | boolean | Price is treated as verified / high-confidence. |
| `accurate` | boolean | Buy/sell spread is tight enough to trust both sides. |
| `suspicious` | boolean | Price was flagged as potentially anomalous. |
| `stn` | boolean | Current buy originates from STN Trading. |
| `new` | boolean | Item was added to the catalog recently. |
| `type` | string | Pricing mode: `free` (automatic), `fixed`, or `manual`. |
| `updatedAt` | string | Unix time in milliseconds of the last pricing pass, as a decimal string. `"0"` means the item is waiting for a pricing pass. |
| `createdAt` | string | ISO-8601 timestamp when the item was first tracked. |
| `price1dAgo` | string or null | `price` snapshot from about 24 hours ago, in keys. |
| `price7dAgo` | string or null | `price` snapshot from about 7 days ago, in keys. |
| `price30dAgo` | string or null | `price` snapshot from about 30 days ago, in keys. |

### Optional raw fields (`raw=true`)

| Field | JSON type | Description |
|---|---|---|
| `raw_price_key` | string or null | Whole keys of `price` |
| `raw_price_ref` | string or null | Leftover refined metal of `price` |
| `raw_buy_key` | string or null | Whole keys of `buyOrder` |
| `raw_buy_ref` | string or null | Leftover refined metal of `buyOrder` |
| `raw_sell_key` | string or null | Whole keys of `sellOrder` |
| `raw_sell_ref` | string or null | Leftover refined metal of `sellOrder` |

### Optional listings object (`listings=true`)

| Field | JSON type | Description |
|---|---|---|
| `observed_at` | string or null | ISO-8601 time of the snapshot |
| `buy` | array | Buy listings, highest price first |
| `sell` | array | Sell listings, lowest price first |

## Listing object

| Field | JSON type | Description |
|---|---|---|
| `steamid` | string | SteamID64 of the buyer or seller |
| `keys` | number or null | Whole keys component of the listing price |
| `metal` | number or null | Refined metal component of the listing price |
| `is_bot` | boolean | Whether the listing account is an automated bot |
| `bumped_at` | number | Unix timestamp (seconds) when the listing was last bumped |
| `source` | string | `bptf`, `stn`, or `ws` |
| `channel` | string or null | `request` (snapshot fetch) or `websocket` |
| `trade_url` | string or null | Steam trade-offer URL when known |
| `details` | string or null | Raw listing description text |
| `stock_text` | string or null | Parsed stock text when present (for example `"3/8"`) |
| `stock_level` | number or null | Current stock count when parsed |
| `stock_limit` | number or null | Maximum stock when parsed |
| `observed_at` | string | ISO-8601 time this row was observed |

## Fetching the full catalog

1. `GET /api/items/count` → `count`
2. For `page` from 1 to `ceil(count / 1000)`: `GET /api/items/{page}`
3. Optional: add `?raw=true` and/or `?listings=true`
4. Stay under 300 requests per minute per IP

There is no public single-item lookup in this REST API. Filter the paginated catalog by `name` or `sku` on the client.

## Website

The live dashboard is at https://autopricer.tf/dashboard. It is a human UI, not the public REST API.

## Contact / identity

- Site: https://autopricer.tf
- Public API docs: https://autopricer.tf/docs
- This Markdown file: https://autopricer.tf/docs.md
