Every bag on this site, as JSON, free and open. Static files rebuilt once a night — no key, no rate limit, no sign-up. If you are an AI agent, start at /llms.txt.
2994 bags of specialty coffee from 157 New Zealand roasters, re-checked every night; last read 08 Oct 2026 at 13:18 UTC. The cheapest bag in stock is Apollo from TOB Coffee at $4.00 per 100g ($40.00 for 1000g). Of retail-sized bags (500g or under) it is Roaster's Pick from Able Coffee Collaborative, 500g at $4.20 per 100g. The biggest price cut is Brazil Ibiraci Peaberry from Cascade Coffee Co., down 31.0% from $29.00 to $20.00. 99 bags arrived since we started, most recently KINGSLAND BLEND from Roasted Addiqtion. 114 of the 2994 are out of stock right now. Prices are the roasters' own; we read them, we do not set them.
| URL | What | Shape |
|---|---|---|
| /api/beans.json | Every bag we track, cheapest per 100g first. | beans[] of Bean. |
| /api/deals.json | Price cuts, new arrivals and the cheapest shelf — each row carrying the sentence that says why it is there. | price_cuts, just_dropped, cheapest_per_100g, each {count, items[], empty, method}, plus summary. |
| /api/roasters.json | Every roaster we have found in New Zealand: the ones we price-track with their shipping terms and where they roast, and the ones we only know of, with the reason. | roasters[] of Roaster (tracked: true), listed[] of ListedRoaster (tracked: false). |
| /api/subscriptions.json | Every roaster's subscription, priced per delivery at each bag size, with the delivery charge kept as its own number. | sizes[] of {key, label, count, method, freight, items[] of Subscription}, plus summary and default_size. |
| /api/schema.json | JSON Schema for all four. | Draft 2020-12. |
| /api/openapi.json | The same, as OpenAPI 3.1. | A spec anyone is free to wrap. We also host the MCP server below, so wrapping it is a choice rather than the only way in. |
| /llms.txt | What this site is, in the emerging convention. | Markdown, at the root. |
POST /mcp | Our own MCP server: the same data as three tools — search_beans, best_deals, roaster_info. No key, no account, read-only. | JSON-RPC over streamable HTTP. Card at /.well-known/mcp/server-card.json. |
One scrape a night at 12:30 UTC (00:30 NZST, 01:30 NZDT), then the whole site and every endpoint is rebuilt.
We host one at https://coffeeaddict.nz/mcp — the same data as 3 read-only tools (search_beans, best_deals, roaster_info), over streamable HTTP. In Claude: Settings › Connectors › Add custom connector, and paste https://coffeeaddict.nz/mcp. Other clients take the same URL as a streamable-HTTP server. There is no key and no account to make, and every tool is read-only.
Server card: https://coffeeaddict.nz/.well-known/mcp/server-card.json
| Field | Means |
|---|---|
generated_at | When this file was built. UTC, ISO-8601. |
next_update_after | Nothing in it changes before this. UTC. |
currency | Always NZD. We do not convert. |
citation | How to credit us, if you quote us. |
terms | The one paragraph that governs all of it. |
disclosure | What our outbound links are and what we earn from them. |
One bag, in /api/beans.json and in every list in /api/deals.json.
| Field | Type | Means |
|---|---|---|
id | string | Stable id, `<roaster-slug>:<product>-<weight>`. Survives price changes. |
roaster | string | The roaster's name, as they write it. |
roaster_slug | string | Their key here, and the last part of their page URL. |
roaster_url | string | Short link to their shop's home page, on our domain. |
name | string | The coffee's name, as the roaster writes it. |
url | string | Where to send a reader: a short link on our domain that redirects to the roaster's product page and counts the click. |
shop_url | string | The roaster's own product page, unredirected — so you can see exactly where `url` lands. |
page | string | Our page for this roaster, where this bag is listed. |
price_nzd | number|null | Shelf price in NZD. Null when we could not read one. |
weight_g | integer|null | Bag weight in grams. Null when the roaster does not state it. |
price_per_100g | number|null | Shelf price per 100g. Null when price or weight is null. |
in_stock | boolean | Whether the roaster showed it as available at the last check. |
origin | string|null | Country or region, where the roaster names one. |
roast_style | string|null | `espresso`, `filter` or `omni`, from the roaster's own words. |
roast_level | string|null | Light/medium/dark, only where the roaster states it. |
flavour_notes | array<string> | The roaster's own tasting notes, verbatim. |
flavours | array<string> | Those notes mapped onto our 18-key flavour vocabulary. |
shipping | object | What it costs to get this bag to a door — see `shipping` below. |
first_seen | string | ISO-8601 UTC. When WE first saw it, not when the roaster listed it. |
last_seen | string | ISO-8601 UTC. The last check that found it. |
last_price_change | string|null | ISO-8601 UTC of the last price MOVE. Null means it has never moved since we started watching — not that we have no history. |
previous_price_nzd | number|null | What it cost before that move. Null if it has never moved. |
The `shipping` object on a Bean: what it costs to get THAT bag to a door.
| Field | Type | Means |
|---|---|---|
published | boolean | Whether the roaster publishes a rate outside checkout. Several do not. |
free_over_nzd | number|null | Order total they ship free at, where they publish one. |
free_over_g | number|null | Order WEIGHT they ship free at, for the roasters whose threshold is a weight rather than a basket total. Either threshold alone is enough. |
flat_nzd | number|null | Their flat rate below that, where they publish one. |
flat_min_g | number|null | Minimum order weight for `flat_nzd` to apply, where they set one. Below it their rate is unpublished — do not apply the flat rate to a lighter order. |
flat_varies | boolean | True where the rate depends on the island; `flat_nzd` is then the cheaper. |
ships_free | boolean | Whether this single bag already clears a free-shipping threshold of either kind — the basket total or the weight. |
delivered_price_nzd | number|null | This bag at the door: price plus shipping. Null when the roaster publishes no rate this bag can use (unmet free-shipping threshold, under-threshold rate unpublished) — the door price is unknowable then, and null cannot be misquoted the way a 'floor' price was. |
delivered_price_exact | boolean | False when `delivered_price_nzd` is a FLOOR — their rate varies by island and we quote its cheapest. Never treat a floor as a price. |
source | string|null | The roaster page a human read the terms off. |
note | string|null | Their terms in their own words, where they publish any. |
last_verified | string|null | The day a human last read that page. Nothing re-checks it. |
One roaster, in /api/roasters.json.
| Field | Type | Means |
|---|---|---|
slug | string | Their key here. |
name | string | Their name. |
url | string | Short link to their shop, on our domain (redirects, counts). |
shop_url | string | Their own URL, unredirected. |
page | string | Our page for them. |
bean_count | integer | Bags of theirs we are tracking. One per size, so a coffee sold in three sizes counts three. |
coffee_count | integer | Distinct coffees behind those bags, sizes collapsed. Never more than `bean_count`. |
cheapest_per_100g | number|null | Their cheapest bag per 100g. |
last_checked | string|null | ISO-8601 UTC of our last successful read of their shop. |
ok | boolean | Whether that last read succeeded. |
stale | boolean | True when we have not managed to check them for 3 days. |
city | string|null | Where they roast, where we have confirmed it. |
place | string|null | Suburb and city, as one line. |
lat | number|null | Latitude, for distance sorting. Null where we could not confirm one. |
lng | number|null | Longitude. |
location_source | string|null | The page a human read the address off. |
shipping | object | Their published shipping terms — see `terms`. |
tracked | boolean | Always true in `roasters`: we read this roaster's shop every night. The `listed` array is the roasters we know of and do not price-track, and every row there is false. |
The `shipping` object on a Roaster: what they publish, not what a bag costs.
| Field | Type | Means |
|---|---|---|
published | boolean | Whether they publish a rate outside checkout. Several roasters do not. |
free_over_nzd | number|null | Order total they ship free at, where they publish one. |
free_over_g | number|null | Order WEIGHT they ship free at, for the roasters whose threshold is a weight rather than a basket total. Either threshold alone is enough. |
flat_nzd | number|null | Their flat rate below that, where they publish one. |
flat_min_g | number|null | Minimum order weight for `flat_nzd` to apply, where they set one. Below it their rate is unpublished — do not apply the flat rate to a lighter order. |
flat_varies | boolean | True where the rate depends on the island; `flat_nzd` is then the cheaper. |
source | string|null | The roaster page a human read the terms off. |
note | string|null | Their terms in their own words, where they publish any. |
last_verified | string|null | The day a human last read that page. Nothing re-checks it. |
One roaster in the `listed` array of /api/roasters.json: we know they exist, we do not read their prices, and `reason` says why. The directory at /roasters/ is the same two tiers as a page.
| Field | Type | Means |
|---|---|---|
name | string | Their name, as our sources write it. |
url | string|null | Short link to their own site, on our domain (redirects, counts). Null where we have found no website for them. |
website | string|null | Their own URL, unredirected. Null where we have none. |
town | string|null | The town or suburb we have them in. |
region | string|null | Their New Zealand region. |
tracked | boolean | Always false here. See `reason` for why. |
reason | string | Why we do not price-track them, in plain words: no website we could find, their site did not answer our check, their robots.txt asks us not to read it, their shop is password-protected, or simply not yet. |
sells_online | boolean|null | Whether they appear to sell beans online at all. Null where we could not tell. |
platform | string|null | What their shop is built on, where we could tell (`shopify`, `woocommerce`, `squarespace`, …). This is how we decide who is cheap to onboard next. |
One roaster's cheapest plan at one bag size, in /api/subscriptions.json.
| Field | Type | Means |
|---|---|---|
roaster_slug | string | Their key here, and the last part of their page URL. |
roaster | string | The roaster's name, as they write it. |
url | string | Where to send a reader: a short link on our domain that redirects to the roaster's subscription listing and counts the click. |
shop_url | string | The roaster's own listing, unredirected — so you can see exactly where `url` lands. |
page | string | Our page for this roaster. |
bag_size_g | integer | The bag this row priced. Rows inside one size view can differ: five roasters sell the small bag as 200g and eight sell it as 250g, and neither is restated as the other. |
bag | string | That weight, written out. |
price_nzd | number | Coffee only, per delivery — the cheapest plan the roaster publishes at this size. |
price_from | boolean | True when their other coffees at this size cost more, so `price_nzd` is a floor rather than the price. |
delivery_nzd | number|null | What freight costs ON THIS ORDER, not their general rate: 0 where the order already clears their free-shipping threshold. Null where they publish no rate that applies to it. |
delivery_published | boolean | Whether that figure came from a published rate. |
delivery_rule | string | The threshold or flat rate that decided it, in words. |
delivered_nzd | number | Coffee plus delivery: what a delivery actually costs. |
delivered_exact | boolean | False when `delivered_nzd` is a FLOOR — the roaster publishes no rate below their threshold, or their other coffees cost more. Never treat a floor as a price. |
delivered_per_100g | number | That total over the grams in the bag. This is what the comparison is ranked on. |
discount_percent | number|null | Measured from the two published prices, never from an advertised percentage. 0 means the roaster sells the plan at shelf price — a real finding. Null means no comparable one-off price existed. The two are never merged. |
every_days | array<integer> | Every delivery cadence offered at this size, in days. The per-delivery price does not move with it. |
kind | string | `subscribe_and_save` (an ordinary bag on repeat, published only at /products/<handle>.js) or `standalone` (the roaster's own subscription listing). |
choose_coffee | boolean | Whether the plan lets the subscriber pick the coffee. |
plan_count | integer | How many plans the roaster publishes at this size. |
commitment | string | `prepaid` (you buy the whole term in advance), `minimum` (rolling, but a stated number of deliveries must arrive before you can cancel) or `rolling`. **`rolling` does NOT mean cancel anytime** — it means the roaster publishes nothing about a minimum term, and almost none of them do outside the checkout. |
term_total_nzd | number|null | What a prepaid plan charges up front, for `deliveries` deliveries. Null on any other plan. This is the number that decides whether the offer is open to a reader at all, which is why prepaid plans are listed separately from the ranking rather than inside it. |
term_deliveries | integer|null | How many deliveries that cheque covers, or the minimum a `minimum` plan ties you to. Null on a rolling plan. |
house | boolean | True only for a product of ours. False on every row today, and a house product is never ranked among the roasters' — see /subscriptions/. |
Prices, weights and stock are the roasters' own, read from their public shop pages and re-checked every night. We do not set them, mark them up, or estimate them: a figure we could not read is null. Free to quote with attribution and a link.
Cite as: Coffee Addict — every specialty coffee bean on sale in New Zealand, priced per 100g and re-checked every night. https://coffeeaddict.nz