Build with the Macfax API.
Everything an agent or app needs to help someone buy or sell a used Mac: serial lookup, sale and asking price statistics per exact configuration, quality-gated live listings with deep links, single-listing trust checks, verified reports, and standing alerts. Free, JSON, no account. Anonymous calls work; a free key raises the limits.
Machine-readable contract: /api/v1/openapi.json · agent index: /llms.txt
Quickstart
# Which Mac, iPad or iPhone is this? curl 'https://macfax.com/api/v1/lookup?serial=C02P73J9G8WP' # What's a used 14" MacBook Pro M3 Pro going for? curl 'https://macfax.com/api/v1/price-stats?config=macbook-pro-14-m3pro-2023' # What's a Mac worth, by the name Apple gives it? curl 'https://macfax.com/api/v1/market?model=MacBook%20Air%20(13-inch,%20M4,%202025)' # Live listings under $1,500, cheapest first curl 'https://macfax.com/api/v1/listings?config=macbook-pro-14-m3pro-2023&max_price_usd=1500&sort=price_asc' # Is this eBay listing trustworthy? curl 'https://macfax.com/api/v1/check-listing?url=https://www.ebay.com/itm/1234567890' # 10x the limits, one request, no email curl -X POST 'https://macfax.com/api/v1/keys'
Every response is { data, meta }. The meta block carries as_of, attribution, and a docs link. Send a key as Authorization: Bearer, X-Api-Key, or ?api_key=.
The endpoints
| Endpoint | What it answers |
|---|---|
| GET /api/v1/lookup?serial=… | Serial to model for any Mac, iPad or iPhone, including 2021+ randomized serials: a status, Apple's device class, the model year where Apple's name carries one, the device's record on Macfax (first seen, how often looked up, any listing or verified report on file), and for a Mac what the model is worth. Lookup identifies; a report proves. |
| GET /api/v1/price-stats?config=… | The sale estimate for a configuration, labeled by basis (verified direct sales vs calibrated from asks), plus the asking band as seller guidance: median/p25/p75 with sample size, per-channel medians and net-to-seller after fees. |
| GET /api/v1/market?model=… | What a Mac is worth from Apple's own model name, e.g. MacBook Pro (14-inch, 2021): every configuration the name can mean, each with its sale estimate and asking band, and one value or a low-to-high range across them. |
| GET /api/v1/listings?config=… | Live listings across eBay, Craigslist, OfferUp, Swappa, Facebook and Reddit, scam/junk/stale-filtered, each with a deep link to the source. |
| GET /api/v1/check-listing?url=… | One listing's trust picture: known, still live, flags, ask vs the typical band, the platform's own seller reputation where it exists (eBay, Swappa), verified report attached. Human twin at /listing-check. |
| GET /api/v1/reports/{id} | A verified Macfax report as JSON. Also served at /r/{id}.json. |
| GET /api/v1/configs/{config} | A config page as data: band, inventory, freshest listings, demand signal. Also served at /shop/{config}.json. |
| GET /api/v1/briefs/{month} | A monthly market brief's full data as CSV (aggregates only). Also served at /market/{month}.csv. |
| POST /api/v1/alerts | A standing watch: daily email when new matching listings appear. Human-confirmed before anything sends. |
| POST /api/v1/keys | Mint a free API key in one request. No email, no account; limits rise about 10x. |
| GET /api/v1/credits | This key's credits, what is billable, and the price list. Read it before a large batch. |
Building it into an app
- Match on model_identifiers, not the model name. Every resolved Mac lists the Apple identifiers the model can be, such as Mac16,6 and Mac16,8 for a 14-inch 2024 MacBook Pro with an M4 Pro or M4 Max, and model_identifier carries the one when there is only one. Model names vary between sources, so treat model as display text. iPads and iPhones come back with device_type and model and an empty identifier list.
- Cache by serial. A resolved answer never changes, so one lookup per serial per device is enough.
- Say why, once. Send context on every lookup when your app knows the reason: inventory, intake, resale, buyer, support or other. It is optional, it never changes the answer, and it is set once in your client.
- Switch off cleanly. Every response carries X-RateLimit-Remaining-Day, and every answered lookup by a paid key carries X-Credits-Remaining. A 429 (honor Retry-After) or a 402 (balance spent) means stop until the headers or GET /api/v1/credits say otherwise.
- Words and typos cost nothing. Anything that cannot be a serial, a model name typed into a search box or an O where a zero belongs, returns 400 invalid_serial without spending a credit.
- Market value goes by the name Apple gives the model. GET /api/v1/market?model= returns a value when the name pins one configuration and a low-to-high range when it leaves the chip open. Macs from before Apple Silicon return 404.
- Keep the key out of the binary. Serve it from a config you control, so replacing it never needs a release.
Running a batch
Identifying a whole fleet of Macs, iPads and iPhones is one loop over the lookup endpoint. Not writing code? The bulk page takes a pasted list or a CSV, runs this loop with your key, and hands back a CSV.
- Check the balance first. GET /api/v1/credits with your key returns the balance and what is billable. Every answered lookup by a paid key repeats the balance in billing.credits_remaining and the X-Credits-Remaining header, and a 402 carries the header.
- One at a time, or at most three in flight. A key may send 100 lookups a minute, and 2,000 a day on a free key or 5,000 on a paid one. A serial we have not seen takes 1.5 to 2.5 seconds and a known one well under a second, so a plain loop clears 1,000 serials in under an hour. Past three in flight, new serials start coming back as retry.
- Branch on status. resolved: store model, device_type and year, plus model_identifiers for a Mac. unknown_serial: no device has that serial, usually a typo. It is an answer, so it costs a credit like any other, and asking again within 24 hours is free. unsupported: a real Apple device whose model we cannot identify yet, never charged. retry: a temporary failure, never charged, so run those again later; the ask that answers is billed like any other serial.
- On a 429, wait and resend the same serial. Sleep for the Retry-After it gives, never a fixed pause: the per-minute limit clears within a minute, but a daily limit, or daily_capacity (the shared daily ceiling on new serial resolutions across every caller), can take hours. Nothing refused is charged.
- A 402 means the credits are spent. Stop, top up, and rerun from the top. Serials this key already had answered in the last 24 hours answer free, so nothing is charged twice.
- Mark the run. Send context=inventory and one run_id, a UUID you mint per run, with every serial in it. Both are optional and never change the answer.
- Send the serial alone. A barcode on Apple's packaging reads S followed by the serial, so drop that S from scanned values; with it, a 2021+ serial reads as a much older one.
- Restarting costs nothing. A crashed run can start again from the first serial: anything this key already had answered in the last 24 hours is free to ask again, so only serials still unanswered cost anything. A 400 is not a serial, and free too.
import time
import uuid
import requests
KEY = "mfx_..." # your key
URL = "https://macfax.com/api/v1/lookup"
RUN = str(uuid.uuid4()) # one per run
def lookup(serial):
while True:
r = requests.get(URL, params={"serial": serial, "context": "inventory", "run_id": RUN},
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
if r.status_code == 429: # a limit, not an error: wait, resend
time.sleep(int(r.headers.get("Retry-After", "60")))
continue
if r.status_code == 402: # credits spent: top up, rerun from the top
raise SystemExit("Out of credits")
if r.status_code == 400: # not a serial: fix the input, nothing charged
return None
r.raise_for_status()
return r.json()["data"]
for serial in serials:
d = lookup(serial)
if d and d["status"] == "resolved":
print(serial, d["device_type"], d["model"], d["year"], d["model_identifiers"])
else: # unknown_serial and unsupported are final; run "retry" again later
print(serial, d["status"] if d else "invalid")Rate limits
Published so nobody has to probe for them. Anonymous limits are per IP; a free key multiplies them. Over-limit calls get an honest 429 with Retry-After and X-RateLimit-* headers, plus a shared per-endpoint daily breaker so a runaway integration degrades one tool for a day rather than everything.
| Tool | Anonymous | With a key | Shared daily cap |
|---|---|---|---|
| lookup | 10/min · 200/day | 100/min · 2000/day | 50,000/day |
| price-stats | 30/min · 1000/day | 300/min · 10000/day | 50,000/day |
| market | 30/min · 1000/day | 300/min · 10000/day | 50,000/day |
| listings | 20/min · 500/day | 200/min · 5000/day | 20,000/day |
| check-listing | 20/min · 500/day | 200/min · 5000/day | 20,000/day |
| reports | 30/min · 1000/day | 300/min · 10000/day | 50,000/day |
| configs | 20/min · 500/day | 200/min · 5000/day | 20,000/day |
| briefs | 30/min · 500/day | 300/min · 5000/day | 20,000/day |
| alerts | 5/min · 20/day | 10/min · 40/day | 25/day |
| keys | 2/min · 5/day | 2/min · 5/day | 200/day |
| credits | 20/min · 500/day | 200/min · 5000/day | 20,000/day |
Paid keys have a free key’s limits, except lookup, which allows 5,000 a day. Credits lift the daily allowances on new resolutions and distinct serials, not these request limits. New serial resolution also has a shared ceiling of 2,000 a day across every caller, which answers 429 daily_capacity when reached. Free callers share 1,500 of it, and the last 500 are held for paid keys.
Need more for an operational use, or interested in market history? Email support@macfax.com.
Credits
Serial lookup is the one operation with paid credits. One credit answers one distinct serial, any Apple device and any year, and the same serial again within 24 hours is free. Answers that come back retry or unsupported are never charged, and neither is any endpoint above other than lookup.
Without credits, anonymous callers get 5 brand new 2021+ resolutions a day, the same as the web lookup. A free key gets 25, and answers up to 200 distinct serials a day. Credits remove both caps, are prepaid, and do not expire. Every lookup response carries a billing block saying whether it cost anything, and every answered lookup by a paid key the balance, in the block and in an X-Credits-Remaining header.
Keys bought before October 8, 2026 keep the earlier unit, one credit per brand new 2021+ resolution, until their next purchase moves them to this one.
| Pack | Credits | Price | Each |
|---|---|---|---|
| api_250 | 250 | $5 | 2.0¢ |
| api_1k | 1,000 | $15 | 1.5¢ |
| api_5k | 5,000 | $60 | 1.2¢ |
Buy with POST /api/v1/credits/checkout, sending your key and a pack name. It returns a Stripe URL to open once. A script can create the session itself and hand a person the link. To pay by invoice instead, email api@macfax.com with your key prefix.
MCP server
The same tools, served over the Model Context Protocol at https://macfax.com/mcp (streamable HTTP, no auth, stateless). Registry name com.macfax/macfax; rate limits are identical to the HTTP API above. Open-source wrapper and install manifests: github.com/macfax/macfax-mcp.
- Claude. Settings → Connectors → add custom connector → https://macfax.com/mcp
- Gemini (Spark). Settings and help → Connected apps → add a custom app → https://macfax.com/mcp
- ChatGPT. Settings → Developer mode → add the endpoint while the directory listing is in review.
- Everything else. Cursor, VS Code, Gemini CLI and friends: install links and a stdio wrapper live in the macfax-mcp repo.
Machine surfaces
- /api/v1/openapi.json · the full contract, stable URL.
- /mcp · the MCP endpoint (streamable HTTP).
- /shop/{config}.json · every config page has a JSON twin.
- /r/{id}.json · every report has a JSON twin.
- /market/{YYYY-MM}.csv · every monthly market brief ships its data as CSV.
- /llms.txt · the curated index of everything above.
Monthly market statistics live at /market. Questions, higher limits, or something you wish existed: support@macfax.com.