Comparing 8,100+ PC parts across 16 UK retailers, new prices read every day.New on the blog: Air cooler or AIO liquid cooler? How to choose a CPU cooler

Skip to content

Developer reference

The VantaSpec API

UK PC hardware, the retailers stocking it, and what each of them charged when we last checked. 128 endpoints, 42 of them needing no credential at all.

Open
42
API key
18
Session
68

Your first call

No account, no key, no signup. Paste this into a terminal.

Search the catalogue
curl "https://vantaspec.com/api/products/search?q=rtx+5090&limit=5"
A few more
# A key raises the rate ceiling on search; it is not required
curl -H "x-api-key: vspec_your_key" \
  "https://vantaspec.com/api/products/search?q=rtx+5090&limit=5"

# Price history for one product, by query parameter
curl "https://vantaspec.com/api/products/price-history?productId=PRODUCT_ID&days=30"

# Categories, and a side-by-side of two products
curl "https://vantaspec.com/api/categories"
curl "https://vantaspec.com/api/compare?slugs=slug-one,slug-two"

Builds, favourites, alerts, reviews and forum posts are session endpoints. There is no key that authorises them; they read the signed-in cookie, so they are callable from the site and not from curl.

Authentication

Three different things are true of different endpoints, so every entry below says which applies to it. Check that badge before you write the request.

open42

No credential at all

Works from curl with no headers. Most read endpoints are here, including the whole catalogue and every price.

key18

An API key is read

On product search a key does not grant access; it raises your rate ceiling. The other two require it.

session68

A signed-in browser session

These read the session cookie and nothing else, so an API key will not authorise them. Anything that writes to your account, your builds or the forum is here.

Request headers
Authorization: Bearer vspec_your_api_key_here

# or, equivalently
x-api-key: vspec_your_api_key_here

Keys are prefixed vspec_ and are created in your profile settings. A key is shown once, at creation, so store it then: it is hashed here and cannot be shown again.

Rate limits

Counted per key, per day, on the endpoints that read one. An anonymous caller on an open endpoint is limited by IP instead.

Free
100
requests / day

Every new API key starts here.

Basic
1,000
requests / day

Personal projects and prototypes.

Pro
10,000
requests / day

Applications with real traffic.

Unlimited
∞
no ceiling

Granted to partners by an admin.

Every limited response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 adds Retry-After. Read them rather than guessing; they are the only account of where you stand.

Endpoints

128 endpoints

Method
Auth

Products & prices

29
GET/api/products/searchkey

Search the catalogue. Open to anonymous callers: a key only raises your rate ceiling, it does not gate access.

Parameters
q
string · Search term
category
string · Category slug
brand
string · Brand name
page
number · Page number (default: 1)
limit
number · Results per page (default: 12, max 50)
inStock
string · 1: only products some shop has in stock
sort
string · price_asc or price_desc: cheapest visible price, priced products only, the first 2,000 matches ranked. Default: newest, best match first within the page
Response
{ products: Product[], total: number, page: number, totalPages: number }
GET/api/search/suggestkey

The top six products and up to three categories for a partly typed search, as the site's search box shows them. Ranked by how well the name answers the words, then by what people opened after the same search and by page views. Open to anonymous callers; a key only raises the rate ceiling. Cached for a minute.

Parameters
qreq
string · What has been typed so far (2 characters or more)
Response
{ products: { slug, name, category, imageUrl, price }[], categories: { slug, name }[] }
POST/api/search/clickkey

Records that a product was opened after a search, which search ranking learns from. Sent by the site for visitors who accepted analytics; stores the query's words and the product, nothing about the visitor. Open to anonymous callers; a key only raises the rate ceiling.

Parameters
qreq
string · The search, as typed
slugreq
string · The live product page that was opened
Response
{ ok: true }, 400 when either is missing, 404 when the product is not live
GET/api/products/price-historyopen

Price history for one product as step series: each retailer's price holds until its next change, runs to now while the retailer still lists it, and the lowest line is the cheapest retailer selling at each moment. New-condition offers only. Read-only.

Parameters
productIdreq
string · Product id
days
number · History window in days (default: 90)
Response
{ series: { retailer, points: { date, price }[], end, live, current }[], lowest: { date, price, retailer }[], from, to, current, low, start, days }
GET/api/extension/cheaperkey

The browser extension's lookup: given a shop's product page that we list, the other shops with the same part in stock for at least £1 less, cheapest first, from prices read in the last 14 days. Matched on the shop's URL, never on the page's name or barcode. Read-only. Open to anonymous callers; a key only raises the rate ceiling.

Parameters
urlreq
string · The shop product page, as in the address bar
price
number · The price shown on that page; without it, the price we last read there is the one to beat
Response
{ match: true, product: { name, url }, here: { retailer, price }, cheaper: { retailer, price, saving, lastChecked }[] } or { match: false, reason: "shop" | "listing" }, 400 when url is not a product page
POST/api/extension/reportopen

The browser extension's admin mode: report the price and stock a shop's product page shows, for the offer we already hold at that exact URL. Developer, admin or owner accounts only, by signed-in session (with the x-vantaspec-extension: 1 header) or a scraper:write key. Dry run unless apply is true. Never creates an offer or a product; writes through the same path and checks as the feed price refresh, and says why when it refuses.

Parameters
urlreq
string · The shop product page
pricereq
number · The page's GBP price from its first schema.org Product
availability
string · That offer's schema.org availability; absent means stock unknown and the stored stock is kept
condition
string · That offer's schema.org itemCondition
apply
boolean · true writes; otherwise only says what would change
Response
{ admin: true, match: true, product: { name, url }, retailer, before: { price, inStock }, decision: { ok, action, price, inStock, stockUnknown } or { ok: false, reason }, applied, after?, shown? } or { admin: true, match: false, reason }; 401 not signed in, 403 not an admin
POST/api/extension/photoopen

The browser extension's admin mode, one photo per call: the image bytes as the body, for the product the exact shop page belongs to. Developer, admin or owner accounts only: a signed-in session with the x-vantaspec-extension: 1 header, or an x-api-key with scraper:write. Runs the scraper image route's checks and stores onto the CDN; max 10 MB.

Parameters
urlreq
string · The shop product page
sourceUrlreq
string · Where the bytes came from
insist
string · 1 to add even when it looks like a photo we already show
Response
{ status: "added" | "duplicate" | "refused", reason?, near? }
POST/api/extension/leadopen

The extension's Not on VantaSpec yet panel: suggest a shop page we do not list. Developer, admin or owner accounts only: a signed-in session with the x-vantaspec-extension: 1 header, or an x-api-key with scraper:write. Dry run unless apply is true. Nothing here touches the catalogue; the scraper's --leads run does that.

Parameters
urlreq
string · The shop page
name
string · Product name
brand
string · Brand
price
number · Price shown
mpn
string · Part number
gtin
string · Barcode
category
string · Category slug
imageUrl
string · Photo
apply
boolean · true saves the lead
Response
{ admin: true, held?, linked?, saved? }
GET/api/extension/leadsopen

The queued leads, oldest first. x-api-key with scraper:write held by a privileged owner.

Parameters
status
string · new, added, rejected or duplicate
limit
number · At most 100
cursor
string · The next value from the last page
Response
{ leads[], next }
PATCH/api/extension/leadsopen

Mark a lead. The scraper's key as above, or a signed-in admin.

Parameters
idreq
string · Lead id
statusreq
string · added, rejected, duplicate or new
note
string · Why
productSlug
string · The product it became
Response
{ ok: true }
GET/api/mobile/deviceopen

This paired phone's name and push toggles; also how the app checks it is still paired. Paired phone only: the x-mobile-token from pairing in /admin/system. Never a session or an API key.

Response
{ device, user }
PUT/api/mobile/deviceopen

Register the phone's Expo push token and save its toggles. Paired phone only: the x-mobile-token from pairing in /admin/system. Never a session or an API key.

Parameters
expoPushToken
string · ExponentPushToken[...] or null
prefs
object · Per-category booleans
name
string · Phone name, 40 characters
Response
{ device }
POST/api/mobile/deviceopen

Send this paired phone a test push and wait briefly for Expo's receipt, so a missing Firebase key is reported. Paired phone only: the x-mobile-token from pairing in /admin/system. Never a session or an API key.

Response
{ ok, sent, errors[], siteAlertsOn }
PUT/api/mobile/watchkey

Price drop alerts for the phone app's watchlist, for any install, no account. Registers or replaces this phone's watched products and the price each was saved at; the alerts cron pushes once per drop (at least £2 or 3%), 8am to 9pm UK time. Stores only the push token and the list; untouched for 60 days, it is deleted. Open to anonymous callers; rate limited, and new phones per address are limited again.

Parameters
tokenreq
string · ExponentPushToken[...]
itemsreq
{ slug, price, target?, restock? }[] · At most 50; price is the cheapest in-stock price when saved, or null; target is an optional "alert me below" price in pounds, which replaces the drop rule for that item: one push when the cheapest in-stock price reaches it, again only after it has gone back above; restock: true alerts only when no shop had it and one does again, never on price
Response
{ ok, watching, unknown[] }
DELETE/api/mobile/watchkey

Turn this phone's price drop alerts off: its row is deleted. Open to anonymous callers; rate limited.

Parameters
tokenreq
string · ExponentPushToken[...]
Response
{ ok: true }
DELETE/api/mobile/deviceopen

Unpair this phone from the phone. Paired phone only: the x-mobile-token from pairing in /admin/system. Never a session or an API key.

Response
{ ok: true }
GET/api/mobile/dealsopen

The phone app's Deals tab: the /deals page's 30-day price drops and biggest shop gaps, 40 of each, and the 12 most popular parts with today's cheapest in-stock price, held five minutes, plus the site's current seasonal theme so the app wears the same accent. Each row carries `displayName`, the product page's heading (the shop title without its own part number). Public, read-only.

Response
{ drops: [...], gaps: [...], popular: [...], season: { id, label, accent, second } }
GET/api/mobile/productkey

One published product for the phone app's product screen: specs, every shop's offer cheapest first with a tracked buy link, and the cheapest in-stock offer. Unpublished products are a 404. Open to anonymous callers like product search: a key only raises the rate ceiling.

Parameters
slugreq
string · The product's slug
Response
{ id, name, displayName, partNumber, slug, brand, model, category, imageUrl, specs, offers, cheapestId }
GET/api/mobile/barcodekey

The phone app's barcode scanner: the published products carrying a barcode, so a box scanned in a shop opens the page with every UK price. EAN-13, UPC-A or GTIN-14, normalised; a code that fails its check digit is a 400. Up to five matches, since a shop can list a code against the wrong part. Open to anonymous callers; a key only raises the rate ceiling. Cached five minutes.

Parameters
codereq
string · The digits under the barcode
Response
{ code, products: { slug, name, displayName, category, imageUrl }[] }
GET/api/mobile/plankey

The Build Planner for the phone app: the same plan as /api/recommendations, with each part's cheapest offer as a tracked buy link. Open to anonymous callers; a key only raises the rate ceiling. Plans are held ten minutes per budget and use.

Parameters
budget
number · Pounds, 300 to 10,000 (default 1,000)
useCase
string · gaming, budget, streaming, content-creation or workstation (default gaming)
Response
{ ok: true, budget, useCase, total, leftover, estimatedDraw, parts: { slot, why, part }[] } | { ok: false, reason, cheapest }
GET/api/mobile/psukey

The PSU calculator for the phone app: the estimated draw of a processor and graphics card plus the rest of the PC, the wattage to buy, and the six cheapest in-stock units at that size with tracked buy links. No wattage is advised when a chosen part's draw cannot be read. Open to anonymous callers; a key only raises the rate ceiling.

Parameters
cpu
string · Processor slug (one of cpu or gpu is required)
gpu
string · Graphics card slug
ram
number · Memory sticks, 0 to 8 (default 2)
ssd
number · SSDs, 0 to 8 (default 1)
hdd
number · Hard disks, 0 to 8 (default 0)
fans
number · Case fans, 0 to 16 (default 3)
aio
string · 1 for an all-in-one liquid cooler
Response
{ parts, extra, draw, recommended, unknown[], readable, psus[] }
GET/api/mobile/moverskey

The weekly price movers for the phone app: the same falls and rises as /price-movers (today's price at a shop selling it against the same shop 7 days ago), each with its photo. Open to anonymous callers; a key only raises the rate ceiling. Rebuilt at most every ten minutes.

Response
{ builtAt, windowDays, falls: { productId, name, displayName, slug, category, categorySlug, was, now, change, shop, imageUrl }[], rises: [...] }
GET/api/mobile/will-it-runkey

What will it run? for one graphics card, for the phone app: its estimated tier (comfortable, playable or struggles) at 1080p, 1440p and 4K, how it compares with a game's published requirements, and the frame rates people measured, as medians once a setup has enough approved results. Also the game, card and resolution lists to pick from. Open to anonymous callers; a key only raises the rate ceiling.

Parameters
game
string · Game slug (default: the first in the list)
res
string · 1080p, 1440p or 4k (default 1440p)
gpu
string · A card's table key or name, e.g. rtx 5070
Response
{ games[], gpus: { key, name }[], resolutions[], game, res, gpu, tiers, tierText, reference, requirements, verdict, fps: Cell[], waiting }
GET/api/mobile/metricsopen

The admin app's dashboard and task list; the same read as the TV, held 45 seconds, plus `needs`: cards on the owner in To do and pending beta requests. Paired phone only: the x-mobile-token from pairing in /admin/system. Never a session or an API key.

Response
{ ...TV metrics, needs: { todoMine, beta } }
GET/api/mobile/notificationsopen

The bell's list for the paired phone, newest first. Paired phone only: the x-mobile-token from pairing in /admin/system. Never a session or an API key.

Parameters
limit
number · At most 50
Response
{ notifications[], unread }
PATCH/api/mobile/notificationsopen

Mark notifications read. Paired phone only: the x-mobile-token from pairing in /admin/system. Never a session or an API key.

Parameters
ids
string[] · Which
all
boolean · true marks every one
Response
{ ok: true }
GET/api/tv/metricskey

The living-room dashboard's one read (/tv). Auth is the pairing token from /admin/system in x-tv-token, never a session or an API key. Read only.

Response
{ ... }
GET/api/products/price-intelligenceopen

Derived price statistics for one product: current, lowest, highest and average over the window.

Parameters
productIdreq
string · Product id
days
number · Window in days (default: 90)
includeRefurbished
boolean · Include second-hand and refurbished offers
Response
{ current, lowest, highest, average, count }
POST/api/products/submitsession

Submit a product for review. Lands as pending with automatic risk flags; it is never published directly.

Response
{ ok: true, id: string, status: "pending" }
Back to filters

Catalogue

3
GET/api/categoriesopen

All 15 categories with product counts.

Response
{ categories: { id, name, slug, productCount }[] }
GET/api/compareopen

Side-by-side data for several products at once.

Parameters
slugsreq
string · Comma-separated product slugs
Response
{ products: Product[], specKeys: string[] }
GET/api/recommendationskey

The best compatible PC the live catalogue can build for a budget, the engine behind /build-planner. Open to anonymous callers; a key only raises the rate ceiling. Tries every priced graphics card and processor model against each other at their cheapest believable listing, fills the rest with the cheapest parts that fit (socket, memory generation, cooler socket, case size, power supply against estimated peak draw), and never relaxes a compatibility rule: when nothing fits it returns ok: false with the cheapest possible total.

Parameters
budgetreq
number · Total budget in GBP, 300 to 10,000
useCase
string · gaming | budget | streaming | content-creation | workstation (default gaming)
Response
{ ok: true, plan: { parts: { slot, part, why }[], total, leftover, estimatedDraw } } | { ok: false, reason, cheapest }
Back to filters

People

1
GET/api/users/searchkey

Find people by handle or display name. Returns only what a public profile already shows.

Parameters
q
string · Handle or display name
page
number · Page number (default: 1)
limit
number · Results per page
Response
{ users: { username, name, image }[], total: number }
Back to filters

Builds

30
GET/api/buildssession

List your builds.

Response
{ builds: Build[] }
POST/api/buildssession

Create a build.

Parameters
namereq
string · Build name
Response
{ id, name, createdAt }
GET/api/builds/[id]open

One build with its parts. Public builds are readable by anyone; a private build returns 403 to everyone but its owner.

Response
{ id, name, parts: BuildPart[], isPublic }
PUT/api/builds/[id]session

Update a build's name, description or visibility. Owner only.

Response
{ ok: true }
DELETE/api/builds/[id]session

Delete a build. Owner only.

Response
{ ok: true }
GET/api/tiktok/connectopen

Post to TikTok (beta): start connecting your own TikTok account. Redirects to TikTok's consent screen and back to the page you came from; signed out, it redirects to sign in instead.

Response
302 redirect
DELETE/api/tiktok/connectsession

Forget your connected TikTok account.

Response
{ ok: true }
GET/api/share/tiktoksession

What the TikTok share page needs for one of your builds: your connected TikTok, the audiences and settings TikTok offers you, the latest video for the build and a suggested caption.

Parameters
buildreq
string · Build id
Response
{ available, account, options, video, caption }
POST/api/share/tiktoksession

Start drawing a video of one of your builds. Poll /api/share/tiktok/[id] for it.

Parameters
buildIdreq
string · Build id
Response
{ id }
GET/api/share/tiktok/[id]session

One build video's progress. Owner only.

Response
{ id, status, error }
GET/api/share/tiktok/[id]/videosession

The rendered build video, for the preview before posting. Owner only.

Response
video/mp4
POST/api/share/tiktok/[id]/postsession

Post a previewed build video to your own TikTok with the audience and settings you chose. A post with no privacy level is refused; nothing is chosen for you.

Parameters
privacyreq
string · One of the levels GET /api/share/tiktok offered
Response
{ ok, publishId }
POST/api/builds/[id]/partssession

Add a part to a build.

Parameters
productIdreq
string · Product to add
Response
{ ok: true }
DELETE/api/builds/[id]/partssession

Remove a part from a build.

Parameters
partIdreq
string · Build part id
Response
{ ok: true }
GET/api/builds/[id]/alertsession

The build's total price alert, its current total and a suggested target. Owner only.

Response
{ alert, total, complete, suggested }
POST/api/builds/[id]/alertsession

Set or change the alert on the build's total. Every part needs a current in-stock price, and the target must be under today's total.

Parameters
targetTotalreq
number · Total in pounds to alert at
Response
{ alert, total }
DELETE/api/builds/[id]/alertsession

Remove the build's total alert.

Response
{ ok: true }
GET/api/builds/[id]/check-compatsession

Compatibility verdict for the build as it stands.

Response
{ issues: { severity, message }[] }
GET/api/builds/[id]/bottlenecksession

CPU/GPU bottleneck estimate for the build.

Response
{ verdict, cpuScore, gpuScore }
GET/api/builds/[id]/suggestionssession

Parts that would improve the build most for the money: the upgrade advisor run on the build's processor, card, memory and supply, the limiting side first. Public builds need no session.

Response
{ ok, limit, why, options: [{ kind, cost, speed, notes, parts: [{ slot, productId, slug, name, imageUrl, price, retailer }] }] }
GET/api/builds/[id]/compatiblesession

Products that fit a given slot on this build.

Parameters
slotreq
string · cpu | gpu | motherboard | ram | ssd | psu | case | cooler
q
string · Search term
limit
number · Results
Response
{ products: Product[] }
GET/api/builds/[id]/commentsopen

Comments on a public build, oldest first (up to 200), each with its author and whether you may delete it. 404 for a private build.

Response
{ comments: [{ id, body, createdAt, byOwner, canDelete, author }], canPost, signedIn }
POST/api/builds/[id]/commentssession

Comment on a public build. Plain text up to 2,000 characters; at most 5 comments in 10 minutes.

Parameters
bodyreq
string · The comment
Response
{ ok: true, id }
DELETE/api/builds/[id]/commentssession

Delete a comment (?comment=<id>): its author, the build's owner, or a moderator.

Parameters
commentreq
string · Comment id (query)
Response
{ ok: true }
POST/api/builds/[id]/votesession

Upvote a public build, or take your upvote back if you had given one. Not your own build (403); a private build is 404. At most 30 votes a minute. The count shows on /builds, whose Most upvoted row lists up to 8 public builds with at least one vote, and on the build page.

Response
{ voted, count }
GET/api/builds/[id]/photossession

Photos of your finished build, each with its review status (pending, approved or rejected). Your own builds only.

Response
{ photos: [{ id, url, width, height, caption, status, createdAt }], max }
POST/api/builds/[id]/photossession

Add a photo of your finished build (multipart form). JPEG, PNG or WebP up to 8 MB; at most 4 per build and 12 a day. Location and camera data are removed. Shown on /builds/gallery once staff approve it and while the build is public.

Parameters
imagereq
file · The photo
caption
string · Up to 200 characters
Response
{ ok: true, photo }
DELETE/api/builds/[id]/photossession

Remove one of your build's photos (?photo=<id>), and its file.

Parameters
photoreq
string · Photo id (query)
Response
{ ok: true }
POST/api/fps-reportssession

Send a frame rate measured on your own PC for /what-will-it-run. Held until staff approve it, and shown only as the median of a game, chip, resolution and preset with at least 3 approved results. At most 10 a day.

Parameters
gamereq
string · A game slug from the page's list, e.g. cyberpunk-2077
resolutionreq
string · 1080p | 1440p | 4k
presetreq
string · low | medium | high | ultra
upscaling
boolean · DLSS, FSR or XeSS on
avgFpsreq
number · Average fps, 1 to 1000
lowFps
number · 1% lows, no higher than the average
productreq
string · Your graphics card's product id
cpu
string · Your processor, up to 100 characters
Response
{ ok: true, report: { id, status } }
GET/api/builds/share/[code]open

Read a build by its share code.

Response
{ build: Build }
Back to filters

Favourites, alerts & reviews

10
POST/api/favoritessession

Toggle a product as a favourite.

Parameters
productIdreq
string · Product id
Response
{ favorited: boolean }
GET/api/alertssession

List your price and back-in-stock alerts.

Response
Alert[]
POST/api/alertssession

Create or replace your alert on a product (one per product). A restock alert fires once, when any shop has it in stock again, then switches off.

Parameters
productIdreq
string · Product id
targetPrice
number · Notify at or below; omit for any drop (price) or any price (restock)
kind
string · "price" (default) or "restock"
Response
{ id, productId, targetPrice, kind, active }
DELETE/api/alertssession

Delete your alert on a product.

Parameters
productIdreq
string · Product id
Response
{ ok: true }
GET/api/saved-searchessession

List your saved search alerts.

Response
SavedSearch[]
POST/api/saved-searchessession

Save a category listing with filters. You hear about products that newly match it, and, if it has a max price, a match falling to that price. Saving the same filters again switches the search back on. Up to 20 per member.

Parameters
categorySlugreq
string · Category slug, e.g. monitors
queryreq
string · The listing's query string, e.g. specs=Refresh Rate:165Hz&maxPrice=200; at least one filter, 1000 characters at most
Response
{ id, categorySlug, query, label, maxPrice, active }
PATCH/api/saved-searchessession

Pause or resume a saved search.

Parameters
idreq
string · Saved search id
activereq
boolean · false to pause, true to resume
Response
{ ok: true }
DELETE/api/saved-searchessession

Delete a saved search.

Parameters
idreq
string · Saved search id
Response
{ ok: true }
POST/api/reviewssession

Post a review.

Parameters
productIdreq
string · Product id
ratingreq
number · 1–5
title
string · Review title
content
string · Review body
Response
{ id, rating, createdAt }
DELETE/api/reviewssession

Delete your own review.

Parameters
idreq
string · Review id
Response
{ ok: true }
Back to filters

Forum

6
GET/api/forum/categoriesopen

Forum categories with thread counts.

Response
{ categories: ForumCategory[] }
GET/api/forum/[categorySlug]open

Threads in a category.

Response
{ threads: Thread[] }
POST/api/forum/[categorySlug]session

Start a thread.

Parameters
titlereq
string · Thread title
contentreq
string · First post
Response
{ id, slug }
GET/api/forum/[categorySlug]/[threadSlug]open

One thread with its posts.

Response
{ thread: Thread, posts: Post[] }
POST/api/forum/[categorySlug]/[threadSlug]session

Reply to a thread.

Parameters
contentreq
string · Reply body
Response
{ id, createdAt }
POST/api/forum/votesession

Vote on a post.

Parameters
postIdreq
string · Post id
valuereq
number · 1 or -1
Response
{ score: number }
Back to filters

Blog

2
GET/api/blogopen

Published blog posts, newest first.

Parameters
page
number · Page number
limit
number · Posts per page
Response
{ posts: Post[], total: number }
GET/api/blog/[slug]open

One blog post by slug.

Response
{ post: Post }
Back to filters

Notifications

3
GET/api/notificationssession

Your notifications, newest first.

Parameters
unread
boolean · Only unread
cursor
string · Pagination cursor
limit
number · Page size
Response
{ notifications: Notification[], nextCursor }
PATCH/api/notificationssession

Mark notifications read.

Parameters
id
string · One id, or omit for all
Response
{ ok: true }
DELETE/api/notificationssession

Delete a notification.

Parameters
idreq
string · Notification id
Response
{ ok: true }
Back to filters

Your account

24
PATCH/api/profilesession

Update your display name, bio or handle.

Response
{ ok: true }
DELETE/api/profile/discordsession

Unlink your Discord account: removes the link, the Verified role on our server and the Discord badge. Refused (409) when Discord is the only way into the account.

Response
{ ok: true, unlinked: boolean }
GET/api/user/api-keyssession

List your API keys, masked. Keys are managed from the browser only: a key cannot manage keys.

Response
[ { id, name, prefix, tier, scopes, rateLimit, requestCount, lastUsedAt, createdAt } ]
POST/api/user/api-keyssession

Create an API key. Maximum 5 active. The full key is shown once and never again.

Parameters
namereq
string · Key name
scopes
string[] · Requested scopes
expiresInDays
number · Expiry in days
Response
{ key: "vspec_...", id, name, prefix, scopes, createdAt }
DELETE/api/user/api-keyssession

Revoke an API key.

Parameters
idreq
string · Key id
Response
{ ok: true }
PUT/api/user/passwordsession

Change your password.

Response
{ ok: true }
GET/api/user/sessionssession

List your active sessions.

Response
{ sessions: Session[] }
DELETE/api/user/sessionssession

Revoke one session, or all but the current one.

Parameters
id
string · Session id
all
boolean · Revoke every other session
Response
{ revoked: number }
GET/api/user/two-factorsession

Two-factor status and enrolment data.

Response
{ enabled: boolean, secret?, qr? }
POST/api/user/two-factorsession

Enable or disable two-factor authentication.

Response
{ enabled: boolean }
POST/api/user/profile-imagesession

Upload a profile image. Processed and stored on the CDN.

Response
{ url: string }
DELETE/api/user/profile-imagesession

Remove your profile image.

Response
{ ok: true }
GET/api/user/data-exportsession

Export everything held about you, as JSON (UK GDPR access request).

Response
{ user, builds, reviews, posts, favorites, alerts }
DELETE/api/user/data-exportsession

Delete your account and its data (UK GDPR erasure request).

Response
{ ok: true }
GET/api/user/email-preferencessession

Your email preferences.

Response
{ newsletter: boolean, priceAlerts: boolean }
PUT/api/user/email-preferencessession

Update your email preferences.

Response
{ ok: true }
GET/api/user/privacysession

Your privacy settings: whether your favourites show on your public profile.

Response
{ favouritesPublic: boolean, username: string | null }
GET/api/user/emailsession

Your email, whether changing it needs your password, and whether the change is confirmed by a link.

Response
{ email: string | null, needsPassword: boolean, verifiesByEmail: boolean }
POST/api/user/emailsession

Change your email. With mail configured, a link goes to the new address and the change happens when it is opened.

Parameters
newEmailreq
string · The new address
password
string · Your password, if the account has one
Response
{ ok: true, pending: boolean, message: string }
GET/api/user/email/confirmopen

The confirmation link from the email. Redirects to account settings.

Parameters
tokenreq
string · From the email
Response
302 to /profile/settings?email=changed|expired|taken
GET/api/pushopen

The public key a browser subscribes to desktop notifications with.

Response
{ publicKey: string }
POST/api/pushsession

Turn on desktop notifications for this browser. Only a Google, Mozilla, Microsoft or Apple push endpoint is accepted.

Parameters
endpointreq
string · PushSubscription.endpoint
Response
{ ok: true }
DELETE/api/pushsession

Turn off desktop notifications for this browser.

Parameters
endpointreq
string · PushSubscription.endpoint
Response
{ ok: true }
PUT/api/user/privacysession

Change your privacy settings.

Parameters
favouritesPublic
boolean · Show your favourites on your public profile
Response
{ favouritesPublic: boolean, username: string | null }
Back to filters

Newsletter

5
POST/api/newsletter/subscribekey

Subscribe an address. Always answers the same regardless of whether the address was already subscribed, so the endpoint cannot be used to test whether someone has an account.

Parameters
emailreq
string · Email address
Response
{ ok: true }
POST/api/newsletter/unsubscribeopen

RFC 8058 one-click unsubscribe, the target of the List-Unsubscribe-Post header. Takes ?token= from the email link; an unknown token still answers ok.

Parameters
tokenreq
string · Query string. The subscriber's token from the email link.
Response
{ ok: true }
POST/api/csp-reportopen

Where browsers report what the Report-Only Content-Security-Policy would block. Counted in memory by directive and host; always 204.

Response
204 No Content
GET/api/newsletter/preferenceopen

Your newsletter preference. Signed out, it answers not subscribed rather than 401.

Response
{ subscribed: boolean, pending: boolean, status }
PUT/api/newsletter/preferencesession

Update your newsletter preference.

Response
{ ok: true }
Back to filters

Sign-in

4
POST/api/auth/registeropen

Create an account.

Response
{ ok: true }
POST/api/auth/pre-loginopen

Pre-flight check before sign-in, reporting whether two-factor is required.

Response
{ twoFactorRequired: boolean }
POST/api/auth/forgot-passwordopen

Begin a password reset. Answers identically whether or not the address exists.

Response
{ ok: true }
POST/api/auth/reset-passwordopen

Complete a password reset with a token.

Response
{ ok: true }
Back to filters

Service

9
GET/api/healthopen

Container probe. Runs SELECT 1 and answers 503 when the database is down.

Response
{ status: "ok" }
GET/api/statusopen

Public service health with history. Unauthenticated on purpose: a status page nobody can read during an outage is not a status page. Exposes only up/down, latency and dates.

Response
{ status, latencyMs, history: { date, status }[] }
GET/api/beta/featuresopen

Beta feature keys this caller may see. An anonymous caller gets the released ones and nothing more, so the response cannot reveal that a disabled feature exists.

Response
{ features: string[] }
GET/api/beta/requestsession

Your beta access request status.

Response
{ status: string | null }
POST/api/beta/requestsession

Request beta access.

Response
{ ok: true }
POST/api/contactopen

Send a message through the contact form.

Response
{ ok: true }
GET/api/storage/[...path]open

Serves an uploaded file. Referenced by image URLs rather than called directly.

Response
the file
POST/api/analytics/trackopen

First-party page-view beacon. Internal: the site calls this itself.

Response
{ ok: true }
POST/api/track/offer-clickopen

Counts an outbound retailer click. Internal: the site calls this itself.

Response
{ ok: true }
Back to filters

Discord bot

1
POST/api/discord/interactionskey

Discord posts every slash command here. A key is accepted only in the sense that it raises the rate-limit ceiling; it is not what guards the route and Discord does not send one. What guards it is an Ed25519 signature over the request's exact bytes, made with the application's private key: anything without a valid one gets 401, and Discord itself probes a newly saved endpoint URL with a deliberately invalid request and refuses the URL unless it answers exactly that. Not useful to call by hand: without a signature there is nothing to see. HTTP interactions rather than a gateway bot, so the bot is up exactly when the site is; the trade is no presence, no reading ordinary messages and no voice. Answers /price (cheapest UK offers for a part) and /deals (where retailers disagree most), both replying only to the person who asked.

Response
{ type: 1 } for the handshake, or { type: 4, data: { content, flags } } for a command
Back to filters

Social cards

1
GET/api/social/cardkey

A generated 1200×630 or 1080×1080 PNG announcing one price, the product photograph with the figures drawn onto it. Open to anonymous callers; a key only raises your rate ceiling. The parameters are claims that are checked, not values that are drawn: the slug must be a published, approved product, the retailer must have a live offer that is not hidden from the site, the price must equal that offer within half a penny, and a `was` must appear in that product's own price history at that retailer. The percentage saved and the "lowest recorded" line are never parameters: they are computed from the rows. Anything that fails returns 409 rather than a picture, so a card can go out of date but cannot go wrong.

Parameters
slugreq
string · Product slug
retailer
string · Which shop's price to quote (default: the cheapest live offer)
price
number · The price being claimed. Must match the live offer; 409 if it has moved
was
number · The price before. Must appear in this product's history at that retailer
size
string · og (1200×630, default), square (1080×1080) or story (1080×1920, for Instagram Stories)
style
string · The look, as five numbers: palette-layout-badge-backdrop-footer (e.g. 3-1-0-5-2). Changes how the figures are drawn, never which figures. Default: 0-0-0-0-0
Response
image/png
Back to filters

Error codes

HTTP status codes this API returns
CodeMeaning
400Bad request: a parameter is missing or malformed.
401Unauthorized: no session on a session endpoint, or an invalid key where one is read.
403Forbidden: signed in, but it is not yours (a private build, someone else’s review).
404Not found: no such resource.
429Rate limit exceeded: wait for Retry-After and try again.
500Something broke on our side.