Veruca tightens a screw in a clockwork dragon on her engineering workbench, blue sparks at its core.

Developer API · Version 1

Build on the Depot’s ledger.

Get the latest Horde scan from the market summary. Its snapshot contains item prices, listed quantities and the scan log. Archived beta markets remain available through the v1 endpoints below.

  • Free · sign in for a key
  • JSON, CSV and Lua
  • Works from the browser (CORS)
  • OpenAPI 3.1
Latest scans from the APIGET /ah/live/api/markets
  • HordeChecking the latest scan…

01 — Quick start

For current prices, read the Normal (PvE) market summary. Replace normal with pvp, roleplaying, roleplaying-pvp or hardcore for another ruleset. A published market is identified by environment, ruleset and auction faction. Its pointer includes the UTC scan time and snapshot hash. Fetch /ah/live/pools/<ruleset>/<house>/snapshot.<snapshotHash>.json for its item prices and quantities. The v1 examples below access the archived beta markets.

Sign in, take a key, ask.

The API is free and requires sign-in. Sign in with Battle.net or Discord, add an email address and create a key. Every endpoint lives under one base address and answers a plain GET carrying that key.

Base address
https://grimlightdepot.com/ah/api/v1

Ask for Linen Cloth (item 2589) on the Horde auction house:

Request
curl -H "X-Grimlight-Key: gdk_your_key" \
  https://grimlightdepot.com/ah/api/v1/markets/horde/items/2589
Response · shortened, values from the September 29, 2026 scan
{
  "market": {
    "id": "horde",
    "name": "Horde",
    "scanTime": "2026-09-29T10:00:48+00:00",
    "snapshot": "f1e3f6ccca747849cf415ce26db1c37f072a2d37ebde3be57ecb613fb9dfa960",
    "priceBasis": "Scanned minimum asking buyout per unit, in copper (10000 copper = 1 gold). Asking prices, not sales or live inventory."
  },
  "item": {
    "id": 2589,
    "name": "Linen Cloth",
    "listed": true,
    "minBuyout": 73,
    "quantity": 7110,
    "previousClose": 35,
    "changePct": 108.57,
    "previousScan": 81,
    "trend": "falling",
    "history": [
      { "day": "2026-09-28", "low": 1, "high": 80, "close": 80, "quantity": 1771, "samples": 35 },
      { "day": "2026-09-29", "low": 18, "high": 85, "close": 73, "quantity": 7110, "samples": 18 }
    ]
  }
}

That reads: the cheapest Linen Cloth was asking 73 copper each, 7,110 were listed, and the scan finished at 10:00 UTC. Every response carries the scan time of the market that answered it.

Keys and sign-in

Free, with a key of your own.

  1. Sign in with Battle.net or Discord on your keys page. You type your password on the provider’s site, never ours.
  2. Add an email address. We need a confirmed way to reach you about your keys and the API. Depot update emails are a separate box that starts unticked.
  3. Create a key for each tool, with a label you’ll recognise. It’s shown once; copy it then. You can revoke it and make a new one any time.
  4. Send it with every request as X-Grimlight-Key: gdk_… (or Authorization: Bearer gdk_…). Tools that can’t send headers, such as Google Sheets, may add ?key=gdk_… to the address.

Name your tool in an X-Grimlight-Client header, for example X-Grimlight-Client: my-addon/1.2, so its usage shows up under that name on your keys page. Keyed requests have no request limit.

Changing over. Requests without a key keep working for now and carry an X-Grimlight-Key-Notice header. After the changeover every data request needs a key. The base address, /openapi.json, /markets, /health and /status stay open.

02 — What the data is

Asking prices from saved scans.

Veruca scans the auction house in game. When the game saves, the scan is checked and published, and this API reads the published result. A few things follow from that, and your tool should be honest about each.

Asking, not sold

minBuyout is the lowest buyout per unit that someone was asking. Nothing here records a sale, a sold price or a profit.

As old as the scan

Scans happen when Veruca plays, so a market can be hours or days old. Read scanTime, and show the age next to any price.

Three separate markets

Horde, Alliance and Neutral are scanned and published on their own. Neutral covers the Booty Bay auctioneer only. One market’s numbers never fill in for another.

  • Money is in copper. 100 copper is 1 silver and 10,000 copper is 1 gold, so minBuyout: 12550 is 1g 25s 50c.
  • Items are base item IDs, the same numbers the game and Wowhead use. Random-suffix variants are not split out.
  • Quantities add up every auction of the item in the scan, including the scanning character’s own.
  • History is one row per observed UTC day. Days without a scan are missing, never filled in. Routine responses include up to 32 recent days plus earlier comparison points, within a byte budget. Complete archived history is available by month through the market's historyArchive.queryUrl; add item=2589&month=2026-10. Months use UTC.
  • Historical items were seen before but not in the latest scan. They have listed: false, no current quote, and lastSeen with lastMinBuyout.

Two comparisons, two meanings. changePct compares minBuyout with previousClose: the item’s last daily close from before the current scanning session, which is what the Auction House heatmap uses. trend compares it with previousScan, the scan just before this one, which may be minutes old. An item can be up on the day and falling since the last scan.

Pick one reference and stick to it. Add compare=previousClose (the default), previousDay, previousScan or dayOpen to /items, the price files or the market summary, and comparePct, the move filter, the movers sort and the movement index all measure against that same quote. A list filtered with move=falling never contains a rise against its own reference.

03 — For addon makers

Getting prices into the game.

Addons can’t reach the web while the game runs, so prices travel as a Lua file. The API writes that file for you, and there are two ways to deliver it.

  1. Ship it with your addon. Download the price file when you build a release and list it in your TOC. Players get the prices that were current when you packaged.
  2. Refresh it with a companion. A small desktop program or script downloads the file into the addon’s folder, and the game loads it at the next login or /reload.
Download the Horde price file
curl -H "X-Grimlight-Key: gdk_your_key" -o Data/Prices-horde.lua "https://grimlightdepot.com/ah/api/v1/markets/horde/prices.lua?var=MyAddonPrices"
What the file holds · first lines
-- Grimlight's Depot - WoW Forever auction house prices
-- Market: Horde - scanned 2026-09-29T10:00:48+00:00
-- Minimum asking buyout per unit in copper. Asking prices from saved scans, not sales.
local data = {
  market = "horde",
  scanTime = 1790676048,
  scanTimeISO = "2026-09-29T10:00:48+00:00",
  snapshot = "f1e3f6ccca747849cf415ce26db1c37f072a2d37ebde3be57ecb613fb9dfa960",
  count = 2059,
  fields = { "minBuyout", "quantity" },
  prices = {
    [2589]={73,7110},
    [2592]={300,4444},
    -- one line per listed item
  },
}
MyAddonPrices = MyAddonPrices or {}
MyAddonPrices["horde"] = data
MyAddon.toc · load the data before your code
## Title: My Addon
Data\Prices-horde.lua
MyAddon.lua
MyAddon.lua · read a price and say how old it is
local market = MyAddonPrices and MyAddonPrices["horde"]

local function AskingPrice(itemID)
  local row = market and market.prices[itemID]
  if not row then return nil end
  return row[1], row[2], market.scanTime   -- copper each, units listed, scan time
end

local price, quantity, scannedAt = AskingPrice(2589)
if price then
  local hours = (time() - scannedAt) / 3600
  print(("Linen Cloth: %dg %ds %dc asking, %d listed, scanned %.0f h ago"):format(
    math.floor(price / 10000), math.floor(price / 100) % 100, price % 100, quantity, hours))
end

Pick your own table name. Pass ?var= with a name that belongs to your addon. If two addons both used the default GrimlightDepotData, whichever loaded last would replace the other’s scan.

Show the age. scanTime is Unix seconds, the same clock as the game’s time(). A price with no age next to it reads as live, and it isn’t.

The file is plain Lua 5.1 and holds only numbers, plus item names if you add names=1. The icon field in the JSON endpoints is an icon FileDataID, which the game’s SetTexture accepts as it is.

04 — Endpoints

Eleven endpoints, all read-only.

Paths below follow the base address. {market} is horde, alliance or neutral, and {id} is an item ID. Parameters you don’t recognize are ignored; parameters with bad values get a 400 that names the parameter.

GET/markets

The three markets: whether each has a published scan, when it was scanned, how old that is, and its snapshot hash. Poll this to notice a new scan. It refreshes every 5 seconds at most.

Response · one market shown
{ "markets": [ {
  "id": "horde", "name": "Horde", "available": true,
  "scanTime": "2026-09-29T10:00:48+00:00",
  "savedTime": "2026-09-29T10:09:13+00:00",
  "publishedAt": "2026-09-29T10:09:32.235Z",
  "ageSeconds": 1316, "stale": false, "items": 2059,
  "snapshot": "f1e3f6cc…",
  "url": "/ah/api/v1/markets/horde"
} ] }

stale turns true when the scan is more than 24 hours old. A market with no published scan has available: false and nothing else.

GET/markets/{market}

One market’s summary: item counts, total listed units and floor value, the movement index against previous close, what the scan covers, and links to the rest. Add ?compare=previousDay, previousScan or dayOpen to get the movement block against that reference instead of previous close; movement.reference says which one answered.

Response · shortened
{ "market": {
  "id": "horde", "scanTime": "2026-09-29T10:00:48+00:00",
  "coverage": "Horde city auction house; retained pre-cutover history, older origin unverified",
  "listedItems": 2059, "historicalItems": 840, "observedDays": 7, "scans": 73,
  "totals": { "floorValue": 143665917, "quantity": 482905, "extremeListings": 0 },
  "movement": { "reference": "previousClose", "indexPct": -11.4,
                "up": 612, "down": 768, "flat": 647, "compared": 2027 }
} }

indexPct weights each item’s change by its previous close × listed units and caps each change at ±50%, so one copper-priced outlier can’t move the market figure. floorValue prices every unit at the lowest buyout: a size measure, not revenue.

GET/markets/{market}/items

Search, filter, sort and page through a market. Without parameters it returns the first 50 listed items by name.

ParameterValuesWhat it does
qtext, up to 64 charactersPart of an item name, any case. A number also matches that item ID.
categorynameExact category, any case. The names come from /categories.
subcategorynameExact subcategory, any case.
classId, subClassIdnumberThe game’s item class and subclass numbers.
quality2 or 3,40 poor, 1 common, 2 uncommon, 3 rare, 4 epic, 5 legendary.
trendrising falling unchanged newDirection against the previous full scan (the trend field).
comparepreviousClose previousDay previousScan dayOpenDefault previousClose. The reference that comparePct, move and the movers sort measure against.
moverising falling unchangedDirection of minBuyout against the compare reference. Items without that reference are left out.
minPrice, maxPricecopperBounds on minBuyout.
ids2589,2592,4306Up to 200 item IDs. Includes historical items unless you set listed.
listedtrue false alltrue (default): in the latest scan. false: historical only. all: both.
sortname id minBuyout quantity floorValue changePct itemLevel quality observedDays movers comparePctDefault name. Items with no value for the field always come last. movers ranks by the size of the move against the compare reference in either direction, so a halving sits beside a doubling; comparePct is the signed change.
orderasc descDefault asc for name and id, desc for everything else.
limit1 to 500Items per page. Default 50.
offset0 or moreItems to skip. Or follow next.
Request
curl "https://grimlightdepot.com/ah/api/v1/markets/horde/items?q=cloth&sort=quantity&limit=5"
Response · shortened
{
  "market": { "id": "horde", "scanTime": "2026-09-29T10:00:48+00:00", "snapshot": "f1e3f6cc…" },
  "compare": "previousClose",
  "total": 28, "limit": 5, "offset": 0,
  "next": "/ah/api/v1/markets/horde/items?limit=5&listed=true&offset=5&q=cloth&sort=quantity",
  "items": [
    { "id": 2589, "name": "Linen Cloth", "minBuyout": 73, "quantity": 7110, "changePct": 108.57, "comparePct": 108.57 },
    { "id": 2592, "name": "Wool Cloth", "minBuyout": 300, "quantity": 4444 }
  ]
}

Each item carries every field in Item fields; the example shows a few. next is null on the last page.

GET/markets/{market}/items/{id}

Everything about one item in one market: the item fields, the in-game tooltip as the scanner captured it, the daily history, and url, its page on the Auction House. Historical items answer too. An item never seen in that market is a 404.

GET/markets/{market}/items/{id}/history

The daily history alone, oldest day first. Narrow it with from and to, both UTC days written YYYY-MM-DD.

FieldMeaning
dayThe UTC day.
low, highLowest and highest minBuyout observed that day.
closeThe last minBuyout observed that day.
quantityUnits listed at that last observation. Can be null in older rows.
samplesHow many scans saw the item that day.
observedAtTime of the last observation that day.
GET/items/{id}

One item in all three markets, side by side. Each market reports its own scan time, and they are usually hours apart, so compare ages before comparing prices. A market that hasn’t seen the item has item: null.

Response · shortened
{
  "id": 10940,
  "markets": {
    "horde":    { "available": true, "market": { "scanTime": "2026-09-29T10:00:48+00:00" }, "item": { "name": "Strange Dust", "minBuyout": 239, "quantity": 7439 } },
    "alliance": { "available": true, "market": { "scanTime": "2026-09-29T05:52:47+00:00" }, "item": { … } },
    "neutral":  { "available": true, "market": { "scanTime": "2026-09-28T05:10:19+00:00" }, "item": { … } }
  }
}
GET/markets/{market}/categories

Category and subcategory names with the number of listed items in each. Use the names with the category and subcategory filters. Names come from the scan as recorded, so a few appear in two forms, such as “Weapon” and “Weapons”; classId is the steadier filter.

GET/markets/{market}/scans

The completed scans kept in the snapshot, oldest first, each with its time and how many items it saw.

GET/markets/{market}/prices.json · .csv · .lua

The whole price table in one download. Use these instead of walking through /items page by page. All three take the same filters as /items, without limit and offset, and sort by item ID unless you ask otherwise.

FileSize nowWhat you get
prices.jsonabout 1.2 MBEvery matching item with all item fields.
prices.csvabout 360 KBA header row, then one row per item. Opens in any spreadsheet. Text that could start a formula gets a leading apostrophe.
prices.luaabout 45 KBA Lua table for addons, listed items only. Takes var (table name) and names=1 (adds item names).

Sizes are for the Horde market on September 29, 2026, before compression. Responses are compressed in transit when your client asks for it.

GET/ and GET/openapi.json

The base address itself returns the endpoint list and the live market summary. /openapi.json is an OpenAPI 3.1 description you can load into Postman, Insomnia or a client generator. /health and /status report whether the API is working and how fresh each market is. None of these need a key.

05 — Item fields

Every field, and what it means.

Items have the same fields everywhere they appear. A field with nothing to report is null, never missing.

FieldTypeMeaning
idnumberBase item ID.
nametextItem name as the game shows it.
category, subcategorytextAuction house grouping, as recorded by the scan.
classId, subClassIdnumberThe game’s item class and subclass.
qualitynumber0 poor to 5 legendary.
itemLevel, minLevelnumberItem level and required level.
inventoryTypenumberThe game’s equipment slot number. 0 for items that aren’t worn.
vendorSellcopperWhat a vendor pays for one.
maxStacknumberLargest stack.
iconnumberIcon FileDataID.
listedtrue / falseWhether the item was in the latest scan.
minBuyoutcopperLowest asking buyout per unit in the latest scan.
quantitynumberUnits listed across all auctions of the item.
floorValuecopperminBuyout × quantity.
extremetrue / falseThe listing’s floor value is over 100 times the market’s 99th percentile: a placeholder or joke price. Left out of totals and the index.
previousClose, previousCloseTimecopper, timeThe item’s last daily close from before the current scanning session, and when it was seen.
changePctnumberminBuyout against previousClose, in percent. Always previous close, whatever compare says.
comparePctnumberLists and price files only. minBuyout against the request’s compare reference, in percent; equal to changePct by default. The response’s top-level compare names the reference.
previousScan, previousQuantitycopper, numberPrice and units in the scan just before this one.
previousDaycopperLast price observed on the UTC day before the scan’s day.
dayOpen, dayOpenTimecopper, timeFirst price observed on the scan’s UTC day.
trendtextrising, falling or unchanged against previousScan; new when that scan had no quote.
observedDaysnumberUTC days on which the item was seen.
lastSeen, lastMinBuyouttime, copperThe last scan that saw the item and its price then. For listed items, that’s the latest scan.
tooltipobjectItem detail only. lines with left and right text and colors, without character-specific lines. null when none was captured.
historylistItem detail only. One row per observed UTC day.

Times are ISO 8601 in UTC, such as 2026-09-29T10:00:48+00:00.

06 — Code examples

Copy, paste, adjust.

JavaScript

Works in a browser page, in Node 18 or later, and in a Discord bot.

One item
const base = 'https://grimlightdepot.com/ah/api/v1';
const headers = { 'X-Grimlight-Key': process.env.GRIMLIGHT_KEY, 'X-Grimlight-Client': 'my-tool/1.0' };

const res = await fetch(`${base}/markets/horde/items/2589`, { headers });
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);

const { market, item } = body;
const hours = (Date.now() - Date.parse(market.scanTime)) / 3.6e6;
console.log(`${item.name}: ${item.minBuyout} copper asking, scanned ${hours.toFixed(1)} h ago`);
Keep a price table fresh without wasting requests
let snapshot = null, prices = new Map();

async function refresh() {
  const { markets } = await (await fetch(`${base}/markets`, { headers })).json();
  const horde = markets.find(m => m.id === 'horde');
  if (!horde.available || horde.snapshot === snapshot) return;   // nothing new

  const table = await (await fetch(`${base}/markets/horde/prices.json`, { headers })).json();
  prices = new Map(table.items.map(item => [item.id, item]));
  snapshot = table.market.snapshot;
}

await refresh();
setInterval(refresh, 60_000);   // one small request a minute

Python

Search a market
import os, requests

BASE = "https://grimlightdepot.com/ah/api/v1"
HEADERS = {"X-Grimlight-Key": os.environ["GRIMLIGHT_KEY"], "X-Grimlight-Client": "my-script/1.0"}

r = requests.get(f"{BASE}/markets/alliance/items", headers=HEADERS,
                 params={"q": "cloth", "sort": "quantity", "limit": 10}, timeout=20)
r.raise_for_status()
data = r.json()

print("Scanned", data["market"]["scanTime"])
for item in data["items"]:
    gold = item["minBuyout"] / 10000
    print(f"{item['id']:>6}  {item['name']:<28} {gold:>9.4f} g  x{item['quantity']}")
Load the whole table into pandas
import pandas as pd

table = pd.read_csv("https://grimlightdepot.com/ah/api/v1/markets/horde/prices.csv", storage_options=HEADERS)
print(table.nlargest(10, "quantity")[["id", "name", "minBuyout", "quantity"]])

PowerShell

Refresh an addon’s price file on Windows
$url  = "https://grimlightdepot.com/ah/api/v1/markets/horde/prices.lua?var=MyAddonPrices"
$file = "C:\Path\To\Interface\AddOns\MyAddon\Data\Prices-horde.lua"
Invoke-WebRequest -Uri $url -OutFile $file -Headers @{ "X-Grimlight-Key" = $env:GRIMLIGHT_KEY }

Google Sheets

One formula in cell A1
=IMPORTDATA("https://grimlightdepot.com/ah/api/v1/markets/horde/prices.csv?key=gdk_your_key")

Sheets can’t send headers, so the key goes in the address. Anyone who can open the sheet can read that formula; use a key made just for the sheet and don’t share it publicly.

The archived beta spreadsheet, Grimlight’s Beta Auction Data, preserves the original beta markets.

07 — Try it

Send a real request.

This runs against the live API from your browser. Paste a key from your keys page to send it with the request; it stays in this tab only.

08 — Caching and fair use

Data changes when a scan lands, not before.

A market’s numbers change only when a new scan is published. Between scans every answer is identical, so asking again gains nothing.

  • Watch the snapshot. Poll /markets no more than once every 30 seconds, and fetch data again only when a market’s snapshot changes.
  • Take the bulk file. If you need more than a few dozen items, download prices.json, prices.csv or prices.lua once per snapshot.
  • Send the ETag back. Every response has an ETag. Return it in If-None-Match and an unchanged answer costs a 304 with no body.
  • Expect a minute of lag. Responses may be reused for 60 seconds (Cache-Control: public, max-age=60), so a new scan can take about a minute to show up.

Requests with a key have no limit. Please still cache by snapshot: it keeps the API fast for everyone. A key used to harm the service or other players can be revoked. Requests without a key, while they still work, are limited to about 120 a minute per address and get 429 with a Retry-After header past that.

Response headers

HeaderMeaning
X-Grimlight-Scan-TimeScan time of the market that answered.
X-Grimlight-SnapshotSnapshot hash that answered.
X-Grimlight-MarketThe market that answered.
ETagChanges only when the content changes.
X-Market-CacheHIT or MISS: whether the answer came from the edge cache.
Access-Control-Allow-Origin*. Any web page may call the API.
X-Request-IdNames this request. Quote it when you report a problem.
X-Grimlight-Key-NoticeSent on keyless data requests during the changeover: get a key.

The API sets no cookies. Each request is counted under its key: the day, the endpoint, the tool name and the result. Your keys page shows your own counts. See Privacy.

09 — Errors

Errors say what went wrong.

400 · bad parameter
{ "error": {
  "code": "invalid_parameter",
  "message": "limit: expected 1 to 500",
  "parameter": "limit",
  "docs": "https://grimlightdepot.com/api/"
} }
StatusCodeWhat to do
400invalid_parameterFix the parameter named in parameter.
404not_foundCheck the path against the endpoint list.
404unknown_marketUse horde, alliance or neutral.
404market_unavailableThat market has no published scan yet. Check /markets later.
404item_not_foundThe item has never been seen in that market. It may still exist in the game.
401key_requiredSend your key in X-Grimlight-Key. Get one free at your keys page.
401invalid_keyThe key is mistyped, revoked or unknown. Copy it again, or create a new one.
405method_not_allowedUse GET or HEAD.
429rate_limitedOnly without a key. Wait for Retry-After seconds, or send a key.
503temporarily_unavailable, snapshot_unavailableTry again in a few seconds. The data you already have is still good.

10 — Use and credit

Free to build with.

The API is free. Use the data in addons, websites, bots, spreadsheets and research. Three requests in return:

  • Credit the source. Say the data comes from Grimlight’s Depot and link to grimlightdepot.com where your tool has room for a link.
  • Keep it honest. Call the numbers asking prices, show when they were scanned, and keep each market under its own name.
  • Be a good neighbor. Keep your key private, cache by snapshot and prefer the bulk price files, so the API stays fast for everyone.

The Depot is a fan project run by one player. The API comes with no guarantee of uptime, freshness or accuracy, and a scan can be wrong or days old. Grimlight’s Depot is not affiliated with or endorsed by Blizzard Entertainment.

Built something, or found a number that looks wrong? Write to veruca@grimlightdepot.com.

11 — Versions

Version 1 stays put.

Inside /v1, fields and endpoints may be added, so ignore fields you don’t know. Nothing is renamed or removed, and no meaning changes. A change that would break your code gets a new address, /v2, and version 1 keeps running beside it.

DateChange
October 2026Added, nothing changed in the data: sign-in with Battle.net and personal keys (X-Grimlight-Key), /health and /status, and the X-Request-Id header. Requests without a key keep working during the changeover.
October 5, 2026Added, nothing changed: compare picks the reference for the new comparePct field, the move filter and the movers and comparePct sorts on /items and the price files; the market summary takes compare too. changePct, trend and the CSV columns are exactly as before.
September 29, 2026Version 1 opens: markets, items, item history, categories, scans, the cross-market item view, and price files in JSON, CSV and Lua.