{"openapi": "3.1.0", "info": {"title": "Coffee Addict — New Zealand coffee prices", "version": "2026-08-23", "description": "Static JSON, regenerated once a night. There are no keys, no rate limits and no POST: every endpoint is a file on a CDN. 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.", "contact": {"url": "https://coffeeaddict.nz/api/"}}, "servers": [{"url": "https://coffeeaddict.nz"}], "paths": {"/api/beans.json": {"get": {"operationId": "listBeans", "summary": "Every bag we track, cheapest per 100g first.", "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"generated_at": {"type": "string", "format": "date-time"}, "next_update_after": {"type": "string", "format": "date-time", "description": "Nothing here changes before this time."}, "source": {"type": "string"}, "documentation": {"type": "string"}, "citation": {"type": "string"}, "terms": {"type": "string"}, "currency": {"type": "string"}, "count": {"type": "integer"}, "roaster_count": {"type": "integer"}, "order": {"type": "string"}, "beans": {"type": "array", "items": {"$ref": "#/components/schemas/Bean"}}}}}}}}}}, "/api/deals.json": {"get": {"operationId": "listDeals", "summary": "Price cuts, new arrivals and the cheapest shelf, each with its reason.", "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"generated_at": {"type": "string", "format": "date-time"}, "next_update_after": {"type": "string", "format": "date-time", "description": "Nothing here changes before this time."}, "source": {"type": "string"}, "documentation": {"type": "string"}, "citation": {"type": "string"}, "terms": {"type": "string"}, "currency": {"type": "string"}, "summary": {"type": "string", "description": "This week on the NZ shelf, in plain sentences."}, "price_cuts": {"type": "object", "properties": {"count": {"type": "integer"}, "items": {"type": "array", "items": {"$ref": "#/components/schemas/Deal"}}, "empty": {"type": "string", "description": "Why the list is empty, when it is."}, "method": {"type": "string", "description": "How the list was decided."}}}, "just_dropped": {"type": "object", "properties": {"count": {"type": "integer"}, "items": {"type": "array", "items": {"$ref": "#/components/schemas/Deal"}}, "empty": {"type": "string", "description": "Why the list is empty, when it is."}, "method": {"type": "string", "description": "How the list was decided."}}}, "cheapest_per_100g": {"type": "object", "properties": {"count": {"type": "integer"}, "items": {"type": "array", "items": {"$ref": "#/components/schemas/Deal"}}, "empty": {"type": "string", "description": "Why the list is empty, when it is."}, "method": {"type": "string", "description": "How the list was decided."}}}}}}}}}}}, "/api/roasters.json": {"get": {"operationId": "listRoasters", "summary": "The roasters we track, with shipping terms and where they roast.", "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"generated_at": {"type": "string", "format": "date-time"}, "next_update_after": {"type": "string", "format": "date-time", "description": "Nothing here changes before this time."}, "source": {"type": "string"}, "documentation": {"type": "string"}, "citation": {"type": "string"}, "terms": {"type": "string"}, "currency": {"type": "string"}, "count": {"type": "integer"}, "roasters": {"type": "array", "items": {"$ref": "#/components/schemas/Roaster"}}}}}}}}}}}, "components": {"schemas": {"Bean": {"type": "object", "title": "Bean", "description": "One bag of coffee.", "additionalProperties": false, "required": ["id", "roaster", "roaster_slug", "roaster_url", "name", "url", "shop_url", "page", "price_nzd", "weight_g", "price_per_100g", "in_stock", "origin", "roast_style", "roast_level", "flavour_notes", "flavours", "shipping", "first_seen", "last_seen", "last_price_change", "previous_price_nzd"], "properties": {"id": {"type": "string", "description": "Stable id, `<roaster-slug>:<product>-<weight>`. Survives price changes."}, "roaster": {"type": "string", "description": "The roaster's name, as they write it."}, "roaster_slug": {"type": "string", "description": "Their key here, and the last part of their page URL."}, "roaster_url": {"type": "string", "description": "Short link to their shop's home page, on our domain."}, "name": {"type": "string", "description": "The coffee's name, as the roaster writes it."}, "url": {"type": "string", "description": "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": {"type": "string", "description": "The roaster's own product page, unredirected — so you can see exactly where `url` lands."}, "page": {"type": "string", "description": "Our page for this roaster, where this bag is listed."}, "price_nzd": {"type": ["number", "null"], "description": "Shelf price in NZD. Null when we could not read one."}, "weight_g": {"type": ["integer", "null"], "description": "Bag weight in grams. Null when the roaster does not state it."}, "price_per_100g": {"type": ["number", "null"], "description": "Shelf price per 100g. Null when price or weight is null."}, "in_stock": {"type": "boolean", "description": "Whether the roaster showed it as available at the last check."}, "origin": {"type": ["string", "null"], "description": "Country or region, where the roaster names one."}, "roast_style": {"type": ["string", "null"], "description": "`espresso`, `filter` or `omni`, from the roaster's own words."}, "roast_level": {"type": ["string", "null"], "description": "Light/medium/dark, only where the roaster states it."}, "flavour_notes": {"type": "array", "items": {"type": "string"}, "description": "The roaster's own tasting notes, verbatim."}, "flavours": {"type": "array", "items": {"type": "string"}, "description": "Those notes mapped onto our 18-key flavour vocabulary."}, "shipping": {"type": "object", "title": "Shipping", "description": "What a roaster charges to deliver, as they publish it.", "additionalProperties": false, "required": ["published", "free_over_nzd", "flat_nzd", "flat_varies", "ships_free", "delivered_price_nzd", "delivered_price_exact", "source", "note", "last_verified"], "properties": {"published": {"type": "boolean", "description": "Whether the roaster publishes a rate outside checkout. Four of thirteen do not."}, "free_over_nzd": {"type": ["number", "null"], "description": "Order total they ship free at, where they publish one."}, "flat_nzd": {"type": ["number", "null"], "description": "Their flat rate below that, where they publish one."}, "flat_varies": {"type": "boolean", "description": "True where the rate depends on the island; `flat_nzd` is then the cheaper."}, "ships_free": {"type": "boolean", "description": "Whether this single bag already clears their free-shipping threshold."}, "delivered_price_nzd": {"type": ["number", "null"], "description": "This bag at the door: price plus shipping."}, "delivered_price_exact": {"type": "boolean", "description": "False when `delivered_price_nzd` is a FLOOR — the roaster publishes no rate, so the true price is this or more. Never treat a floor as a price."}, "source": {"type": ["string", "null"], "description": "The roaster page a human read the terms off."}, "note": {"type": ["string", "null"], "description": "Their terms in their own words, where they publish any."}, "last_verified": {"type": ["string", "null"], "description": "The day a human last read that page. Nothing re-checks it."}}}, "first_seen": {"type": "string", "description": "ISO-8601 UTC. When WE first saw it, not when the roaster listed it."}, "last_seen": {"type": "string", "description": "ISO-8601 UTC. The last check that found it."}, "last_price_change": {"type": ["string", "null"], "description": "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": {"type": ["number", "null"], "description": "What it cost before that move. Null if it has never moved."}}}, "Roaster": {"type": "object", "title": "Roaster", "description": "One roaster.", "additionalProperties": false, "required": ["slug", "name", "url", "shop_url", "page", "bean_count", "coffee_count", "cheapest_per_100g", "last_checked", "ok", "stale", "city", "place", "lat", "lng", "location_source", "shipping"], "properties": {"slug": {"type": "string", "description": "Their key here."}, "name": {"type": "string", "description": "Their name."}, "url": {"type": "string", "description": "Short link to their shop, on our domain (redirects, counts)."}, "shop_url": {"type": "string", "description": "Their own URL, unredirected."}, "page": {"type": "string", "description": "Our page for them."}, "bean_count": {"type": "integer", "description": "Bags of theirs we are tracking. One per size, so a coffee sold in three sizes counts three."}, "coffee_count": {"type": "integer", "description": "Distinct coffees behind those bags, sizes collapsed. Never more than `bean_count`."}, "cheapest_per_100g": {"type": ["number", "null"], "description": "Their cheapest bag per 100g."}, "last_checked": {"type": ["string", "null"], "description": "ISO-8601 UTC of our last successful read of their shop."}, "ok": {"type": "boolean", "description": "Whether that last read succeeded."}, "stale": {"type": "boolean", "description": "True when we have not managed to check them for 3 days."}, "city": {"type": ["string", "null"], "description": "Where they roast, where we have confirmed it."}, "place": {"type": ["string", "null"], "description": "Suburb and city, as one line."}, "lat": {"type": ["number", "null"], "description": "Latitude, for distance sorting. Null where we could not confirm one."}, "lng": {"type": ["number", "null"], "description": "Longitude."}, "location_source": {"type": ["string", "null"], "description": "The page a human read the address off."}, "shipping": {"type": "object", "title": "Terms", "description": "A roaster's published shipping terms.", "additionalProperties": false, "required": ["published", "free_over_nzd", "flat_nzd", "flat_varies", "source", "note", "last_verified"], "properties": {"published": {"type": "boolean", "description": "Whether they publish a rate outside checkout. Four of thirteen do not."}, "free_over_nzd": {"type": ["number", "null"], "description": "Order total they ship free at, where they publish one."}, "flat_nzd": {"type": ["number", "null"], "description": "Their flat rate below that, where they publish one."}, "flat_varies": {"type": "boolean", "description": "True where the rate depends on the island; `flat_nzd` is then the cheaper."}, "source": {"type": ["string", "null"], "description": "The roaster page a human read the terms off."}, "note": {"type": ["string", "null"], "description": "Their terms in their own words, where they publish any."}, "last_verified": {"type": ["string", "null"], "description": "The day a human last read that page. Nothing re-checks it."}}}}}, "Deal": {"type": "object", "title": "Deal", "description": "A bag, with the reason it is on this list.", "additionalProperties": false, "required": ["id", "roaster", "roaster_slug", "roaster_url", "name", "url", "shop_url", "page", "price_nzd", "weight_g", "price_per_100g", "in_stock", "origin", "roast_style", "roast_level", "flavour_notes", "flavours", "shipping", "first_seen", "last_seen", "last_price_change", "previous_price_nzd", "why"], "properties": {"id": {"type": "string", "description": "Stable id, `<roaster-slug>:<product>-<weight>`. Survives price changes."}, "roaster": {"type": "string", "description": "The roaster's name, as they write it."}, "roaster_slug": {"type": "string", "description": "Their key here, and the last part of their page URL."}, "roaster_url": {"type": "string", "description": "Short link to their shop's home page, on our domain."}, "name": {"type": "string", "description": "The coffee's name, as the roaster writes it."}, "url": {"type": "string", "description": "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": {"type": "string", "description": "The roaster's own product page, unredirected — so you can see exactly where `url` lands."}, "page": {"type": "string", "description": "Our page for this roaster, where this bag is listed."}, "price_nzd": {"type": ["number", "null"], "description": "Shelf price in NZD. Null when we could not read one."}, "weight_g": {"type": ["integer", "null"], "description": "Bag weight in grams. Null when the roaster does not state it."}, "price_per_100g": {"type": ["number", "null"], "description": "Shelf price per 100g. Null when price or weight is null."}, "in_stock": {"type": "boolean", "description": "Whether the roaster showed it as available at the last check."}, "origin": {"type": ["string", "null"], "description": "Country or region, where the roaster names one."}, "roast_style": {"type": ["string", "null"], "description": "`espresso`, `filter` or `omni`, from the roaster's own words."}, "roast_level": {"type": ["string", "null"], "description": "Light/medium/dark, only where the roaster states it."}, "flavour_notes": {"type": "array", "items": {"type": "string"}, "description": "The roaster's own tasting notes, verbatim."}, "flavours": {"type": "array", "items": {"type": "string"}, "description": "Those notes mapped onto our 18-key flavour vocabulary."}, "shipping": {"type": "object", "title": "Shipping", "description": "What a roaster charges to deliver, as they publish it.", "additionalProperties": false, "required": ["published", "free_over_nzd", "flat_nzd", "flat_varies", "ships_free", "delivered_price_nzd", "delivered_price_exact", "source", "note", "last_verified"], "properties": {"published": {"type": "boolean", "description": "Whether the roaster publishes a rate outside checkout. Four of thirteen do not."}, "free_over_nzd": {"type": ["number", "null"], "description": "Order total they ship free at, where they publish one."}, "flat_nzd": {"type": ["number", "null"], "description": "Their flat rate below that, where they publish one."}, "flat_varies": {"type": "boolean", "description": "True where the rate depends on the island; `flat_nzd` is then the cheaper."}, "ships_free": {"type": "boolean", "description": "Whether this single bag already clears their free-shipping threshold."}, "delivered_price_nzd": {"type": ["number", "null"], "description": "This bag at the door: price plus shipping."}, "delivered_price_exact": {"type": "boolean", "description": "False when `delivered_price_nzd` is a FLOOR — the roaster publishes no rate, so the true price is this or more. Never treat a floor as a price."}, "source": {"type": ["string", "null"], "description": "The roaster page a human read the terms off."}, "note": {"type": ["string", "null"], "description": "Their terms in their own words, where they publish any."}, "last_verified": {"type": ["string", "null"], "description": "The day a human last read that page. Nothing re-checks it."}}}, "first_seen": {"type": "string", "description": "ISO-8601 UTC. When WE first saw it, not when the roaster listed it."}, "last_seen": {"type": "string", "description": "ISO-8601 UTC. The last check that found it."}, "last_price_change": {"type": ["string", "null"], "description": "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": {"type": ["number", "null"], "description": "What it cost before that move. Null if it has never moved."}, "why": {"type": "string", "description": "Why this bag is on this list, in a sentence."}, "drop_percent": {"type": "number", "description": "Size of the price cut, where there was one."}}}, "Shipping": {"type": "object", "title": "Shipping", "description": "What a roaster charges to deliver, as they publish it.", "additionalProperties": false, "required": ["published", "free_over_nzd", "flat_nzd", "flat_varies", "ships_free", "delivered_price_nzd", "delivered_price_exact", "source", "note", "last_verified"], "properties": {"published": {"type": "boolean", "description": "Whether the roaster publishes a rate outside checkout. Four of thirteen do not."}, "free_over_nzd": {"type": ["number", "null"], "description": "Order total they ship free at, where they publish one."}, "flat_nzd": {"type": ["number", "null"], "description": "Their flat rate below that, where they publish one."}, "flat_varies": {"type": "boolean", "description": "True where the rate depends on the island; `flat_nzd` is then the cheaper."}, "ships_free": {"type": "boolean", "description": "Whether this single bag already clears their free-shipping threshold."}, "delivered_price_nzd": {"type": ["number", "null"], "description": "This bag at the door: price plus shipping."}, "delivered_price_exact": {"type": "boolean", "description": "False when `delivered_price_nzd` is a FLOOR — the roaster publishes no rate, so the true price is this or more. Never treat a floor as a price."}, "source": {"type": ["string", "null"], "description": "The roaster page a human read the terms off."}, "note": {"type": ["string", "null"], "description": "Their terms in their own words, where they publish any."}, "last_verified": {"type": ["string", "null"], "description": "The day a human last read that page. Nothing re-checks it."}}}, "Terms": {"type": "object", "title": "Terms", "description": "A roaster's published shipping terms.", "additionalProperties": false, "required": ["published", "free_over_nzd", "flat_nzd", "flat_varies", "source", "note", "last_verified"], "properties": {"published": {"type": "boolean", "description": "Whether they publish a rate outside checkout. Four of thirteen do not."}, "free_over_nzd": {"type": ["number", "null"], "description": "Order total they ship free at, where they publish one."}, "flat_nzd": {"type": ["number", "null"], "description": "Their flat rate below that, where they publish one."}, "flat_varies": {"type": "boolean", "description": "True where the rate depends on the island; `flat_nzd` is then the cheaper."}, "source": {"type": ["string", "null"], "description": "The roaster page a human read the terms off."}, "note": {"type": ["string", "null"], "description": "Their terms in their own words, where they publish any."}, "last_verified": {"type": ["string", "null"], "description": "The day a human last read that page. Nothing re-checks it."}}}}}}