API Documentation

Integrate luxury watch market intelligence into your app in minutes

30-Second Quickstart

1. Create a free account and copy your API key from the email we send you.

2. Make your first request — replace YOUR_API_KEY with your key:

curl "https://pricethat.watch/api/v1/valuation?brand=Rolex&reference=116500LN" \
  -H "X-Api-Key: YOUR_API_KEY"

3. You'll get a JSON response with market_summary.average_price, floor_price, ceiling_price, and sample_size. See the full response format →

Authentication

Most endpoints require an API key passed in the X-Api-Key header. Anonymous requests are allowed on /api/v1/valuation and /api/v1/brands but are subject to the Free rate limit (10 requests/week).

Paste your key below and it will be injected into all code examples and the live playground.

curl https://pricethat.watch/api/v1/valuation \
  -H "X-Api-Key: YOUR_API_KEY" \
  -G -d "brand=Rolex" -d "reference=116500LN"
import requests

headers = {"X-Api-Key": "YOUR_API_KEY"}
params  = {"brand": "Rolex", "reference": "116500LN"}

r = requests.get(
    "https://pricethat.watch/api/v1/valuation",
    headers=headers, params=params
)
data = r.json()
print(data["market_summary"]["average_price"])
const res = await fetch(
  "https://pricethat.watch/api/v1/valuation?brand=Rolex&reference=116500LN",
  { headers: { "X-Api-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.market_summary.average_price);
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => "https://pricethat.watch/api/v1/valuation?brand=Rolex&reference=116500LN",
    CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_API_KEY"],
    CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
echo $data["market_summary"]["average_price"];

Rate Limits

PlanLimitWindow
Free / Anonymous10 requestsper week
Basic ($30/mo)500 requestsper week
Pro ($99/mo)Unlimited

Rate limit headers are returned on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix timestamp).

Check your current usage anytime with GET /api/v1/usage.

GET /valuation

Returns a market valuation for a specific watch reference number, aggregating live data from up to 8 marketplaces including Chrono24, eBay, WatchBox, Jomashop, and more. Results are cached for 24 hours.

GET /api/v1/valuation

Parameters

ParameterDescription
brandrequiredWatch brand, e.g. Rolex, Patek Philippe
referencerequiredReference number, e.g. 116500LN, 5711/1A
stateoptionalUS state code for tax calculation, e.g. CA, NY
materialoptionalCase material filter: steel, gold, platinum, titanium, ceramic, two-tone
movementoptionalMovement type, e.g. automatic, manual, quartz
dial_coloroptionalDial color filter, e.g. black, blue
detailedoptionalSet to true to include full listing data in the response

Try It

Code Examples

curl "https://pricethat.watch/api/v1/valuation?brand=Rolex&reference=116500LN" \
  -H "X-Api-Key: YOUR_API_KEY"
import requests

r = requests.get(
    "https://pricethat.watch/api/v1/valuation",
    headers={"X-Api-Key": "YOUR_API_KEY"},
    params={"brand": "Rolex", "reference": "116500LN"},
)
data = r.json()
ms = data["market_summary"]
print(f"Floor:   ${ms['floor_price']:,}")
print(f"Average: ${ms['average_price']:,}")
print(f"Ceiling: ${ms['ceiling_price']:,}")
const params = new URLSearchParams({ brand: "Rolex", reference: "116500LN" });
const res = await fetch(
  `https://pricethat.watch/api/v1/valuation?${params}`,
  { headers: { "X-Api-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
const { floor_price, average_price, ceiling_price } = data.market_summary;
console.log({ floor_price, average_price, ceiling_price });
$url = "https://pricethat.watch/api/v1/valuation?" . http_build_query([
    "brand"     => "Rolex",
    "reference" => "116500LN",
]);
$opts = ["http" => ["header" => "X-Api-Key: YOUR_API_KEY"]];
$data = json_decode(file_get_contents($url, false, stream_context_create($opts)), true);
echo "Average: $" . number_format($data["market_summary"]["average_price"]);

GET /valuation-from-url

Accepts a listing URL from eBay, Facebook Marketplace, or Craigslist and returns a valuation for the watch in that listing. The watch brand and reference are extracted automatically from the listing.

GET /api/v1/valuation-from-url

Parameters

ParameterDescription
urlrequiredListing URL, e.g. an eBay item URL or Facebook Marketplace listing
curl "https://pricethat.watch/api/v1/valuation-from-url?url=https%3A%2F%2Fwww.ebay.com%2Fitm%2F123456789" \
  -H "X-Api-Key: YOUR_API_KEY"
const listingUrl = "https://www.ebay.com/itm/123456789";
const res = await fetch(
  `/api/v1/valuation-from-url?url=${encodeURIComponent(listingUrl)}`,
  { headers: { "X-Api-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.product, data.market_summary);

POST /valuation/batch

Submit multiple watch valuations in a single request. Requires Basic or Pro tier. Returns an array of valuation results in the same order as the request.

POST /api/v1/valuation/batch

Request Body

FieldDescription
watchesrequiredArray of objects, each with brand and reference fields
curl -X POST "https://pricethat.watch/api/v1/valuation/batch" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "watches": [
      { "brand": "Rolex", "reference": "116500LN" },
      { "brand": "Patek Philippe", "reference": "5711/1A-010" }
    ]
  }'
const res = await fetch("/api/v1/valuation/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "YOUR_API_KEY",
  },
  body: JSON.stringify({
    watches: [
      { brand: "Rolex", reference: "116500LN" },
      { brand: "Patek Philippe", reference: "5711/1A-010" },
    ],
  }),
});
const { results } = await res.json();
results.forEach(r => console.log(r.product, r.market_summary));

GET /brands

Returns the list of all supported watch brands. No authentication required.

GET /api/v1/brands
curl "https://pricethat.watch/api/v1/brands"
import requests
r = requests.get("https://pricethat.watch/api/v1/brands")
print(r.json()["brands"])
const res = await fetch("/api/v1/brands");
const { brands } = await res.json();
console.log(brands);

GET /usage

Returns your current week's usage statistics for the authenticated API key.

GET /api/v1/usage
curl "https://pricethat.watch/api/v1/usage" \
  -H "X-Api-Key: YOUR_API_KEY"
const res = await fetch("/api/v1/usage", {
  headers: { "X-Api-Key": "YOUR_API_KEY" },
});
const { used, limit, tier, reset_at } = await res.json();
console.log(`${used} / ${limit ?? "Unlimited"} used — resets ${reset_at}`);

Response

{
  "used": 12,
  "limit": 500,
  "tier": "basic",
  "reset_at": "2026-03-27T00:00:00.000Z"
}

For Pro accounts, limit is null (unlimited).

POST /keys/rotate

Invalidates your current API key and returns a new one. The old key stops working immediately.

POST /api/v1/keys/rotate
curl -X POST "https://pricethat.watch/api/v1/keys/rotate" \
  -H "X-Api-Key: YOUR_API_KEY"
const res = await fetch("/api/v1/keys/rotate", {
  method: "POST",
  headers: { "X-Api-Key": "YOUR_API_KEY" },
});
const { key } = await res.json();
console.log("New key:", key);

POST /keys/resend

Sends your API key to your registered email address. Useful if you have lost your key. No authentication header required.

POST /api/v1/keys/resend

Request Body

FieldDescription
emailrequiredThe email address associated with your account
curl -X POST "https://pricethat.watch/api/v1/keys/resend" \
  -H "Content-Type: application/json" \
  -d '{ "email": "you@example.com" }'
const res = await fetch("/api/v1/keys/resend", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "you@example.com" }),
});
const { message } = await res.json();
console.log(message);

POST /billing/portal

Returns a Stripe billing portal URL where you can manage your payment method, view invoices, or cancel your subscription.

POST /api/v1/billing/portal
curl -X POST "https://pricethat.watch/api/v1/billing/portal" \
  -H "X-Api-Key: YOUR_API_KEY"
const res = await fetch("/api/v1/billing/portal", {
  method: "POST",
  headers: { "X-Api-Key": "YOUR_API_KEY" },
});
const { url } = await res.json();
window.location.href = url; // redirect to Stripe portal

POST /billing/change-plan

Upgrades a free account by returning a Stripe Checkout URL, or changes an existing paid subscription immediately. Proration is handled by Stripe.

POST /api/v1/billing/change-plan

Request Body

FieldDescription
tierrequired"basic" or "pro" — the target subscription tier
curl -X POST "https://pricethat.watch/api/v1/billing/change-plan" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tier": "pro" }'
const res = await fetch("/api/v1/billing/change-plan", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "YOUR_API_KEY",
  },
  body: JSON.stringify({ tier: "pro" }),
  credentials: "include",
});
const data = await res.json();
if (res.ok) console.log(data.message);
else console.error(data.error?.message);

DELETE /account

Permanently deletes your account and cancels any active Stripe subscription. This action cannot be undone.

DELETE /api/v1/account
curl -X DELETE "https://pricethat.watch/api/v1/account" \
  -H "X-Api-Key: YOUR_API_KEY"
const res = await fetch("/api/v1/account", {
  method: "DELETE",
  headers: { "X-Api-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.message); // "Account deleted."

Response Format

All responses are JSON. A successful GET /valuation response looks like:

{
  "product": {
    "brand": "Rolex",
    "reference": "116500LN",
    "model": "Cosmograph Daytona"
  },
  "market_summary": {
    "average_price": 28500,
    "floor_price": 24000,
    "ceiling_price": 35000,
    "sample_size": 42,
    "volatility": "medium"
  },
  "authenticity_check": {
    "verdict": "likely_authentic",
    "confidence": "high",
    "flags": ["Dial text quality appears consistent with genuine examples"],
    "reasoning": "External features are consistent with an authentic Rolex Daytona."
  },
  "tax_considerations": {
    "state": "CA",
    "rate": 0.0725,
    "estimated_tax": 2066,
    "total_with_tax": 30566
  },
  "meta": {
    "cached": false,
    "sources_successful": ["Chrono24", "eBay"],
    "data_quality": "high",
    "updated_at": "2026-03-20T03:00:00.000Z"
  }
}

Volatility Values

ValueMeaning
lowPrices are tightly clustered; stable market
mediumModerate price spread (13–30% above floor)
highWide price range; market is volatile (>30% above floor)

Errors

All errors follow a consistent envelope:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Weekly limit of 10 requests reached. Resets 2026-03-27.",
    "upgrade_url": "/pricing.html"
  }
}
HTTPCodeMeaning
400INVALID_PARAMSMissing or invalid query parameters
401UNAUTHORIZEDInvalid or missing API key
403FORBIDDENTier does not allow this endpoint (e.g. batch requires Basic+)
404NOT_FOUNDNo listings found for this reference
429RATE_LIMIT_EXCEEDEDWeekly request limit reached
500INTERNAL_ERRORServer error — try again later