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.
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:
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.
Sign in with Battle.net or Discord on your keys page. You type your password on the provider’s site, never ours.
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.
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.
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.
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.
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.
-- 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 endreturn row[1], row[2], market.scanTime -- copper each, units listed, scan timeendlocal price, quantity, scannedAt = AskingPrice(2589)
if price thenlocal 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.
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.
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.
Parameter
Values
What it does
q
text, up to 64 characters
Part of an item name, any case. A number also matches that item ID.
category
name
Exact category, any case. The names come from /categories.
Default 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.
order
ascdesc
Default asc for name and id, desc for everything else.
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.
Field
Meaning
day
The UTC day.
low, high
Lowest and highest minBuyout observed that day.
close
The last minBuyout observed that day.
quantity
Units listed at that last observation. Can be null in older rows.
samples
How many scans saw the item that day.
observedAt
Time 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.
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.
File
Size now
What you get
prices.json
about 1.2 MB
Every matching item with all item fields.
prices.csv
about 360 KB
A header row, then one row per item. Opens in any spreadsheet. Text that could start a formula gets a leading apostrophe.
prices.lua
about 45 KB
A 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/andGET/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.
Field
Type
Meaning
id
number
Base item ID.
name
text
Item name as the game shows it.
category, subcategory
text
Auction house grouping, as recorded by the scan.
classId, subClassId
number
The game’s item class and subclass.
quality
number
0 poor to 5 legendary.
itemLevel, minLevel
number
Item level and required level.
inventoryType
number
The game’s equipment slot number. 0 for items that aren’t worn.
vendorSell
copper
What a vendor pays for one.
maxStack
number
Largest stack.
icon
number
Icon FileDataID.
listed
true / false
Whether the item was in the latest scan.
minBuyout
copper
Lowest asking buyout per unit in the latest scan.
quantity
number
Units listed across all auctions of the item.
floorValue
copper
minBuyout × quantity.
extreme
true / false
The 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, previousCloseTime
copper, time
The item’s last daily close from before the current scanning session, and when it was seen.
changePct
number
minBuyout against previousClose, in percent. Always previous close, whatever compare says.
comparePct
number
Lists 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, previousQuantity
copper, number
Price and units in the scan just before this one.
previousDay
copper
Last price observed on the UTC day before the scan’s day.
dayOpen, dayOpenTime
copper, time
First price observed on the scan’s UTC day.
trend
text
rising, falling or unchanged against previousScan; new when that scan had no quote.
observedDays
number
UTC days on which the item was seen.
lastSeen, lastMinBuyout
time, copper
The last scan that saw the item and its price then. For listed items, that’s the latest scan.
tooltip
object
Item detail only. lines with left and right text and colors, without character-specific lines. null when none was captured.
history
list
Item 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 newconst 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']}")
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.
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
Header
Meaning
X-Grimlight-Scan-Time
Scan time of the market that answered.
X-Grimlight-Snapshot
Snapshot hash that answered.
X-Grimlight-Market
The market that answered.
ETag
Changes only when the content changes.
X-Market-Cache
HIT or MISS: whether the answer came from the edge cache.
Access-Control-Allow-Origin
*. Any web page may call the API.
X-Request-Id
Names this request. Quote it when you report a problem.
X-Grimlight-Key-Notice
Sent 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.
That market has no published scan yet. Check /markets later.
404
item_not_found
The item has never been seen in that market. It may still exist in the game.
401
key_required
Send your key in X-Grimlight-Key. Get one free at your keys page.
401
invalid_key
The key is mistyped, revoked or unknown. Copy it again, or create a new one.
405
method_not_allowed
Use GET or HEAD.
429
rate_limited
Only without a key. Wait for Retry-After seconds, or send a key.
503
temporarily_unavailable, snapshot_unavailable
Try 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.
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.
Date
Change
October 2026
Added, 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, 2026
Added, 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, 2026
Version 1 opens: markets, items, item history, categories, scans, the cross-market item view, and price files in JSON, CSV and Lua.