---
title: The Coffee Addict API
url: https://coffeeaddict.nz/api/
source: Coffee Addict
checked: Prices checked 23 Aug 2026, 15:40 UTC.
generated: 2026-08-23 23:25 UTC
description: Every bag we track, as JSON: schema, terms and endpoints.
---

# The Coffee Addict API

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.

One scrape a night at 15:40 UTC (about 03:40 in New Zealand), then the whole site and every endpoint is rebuilt.

## Endpoints

| URL | What | Shape |
|---|---|---|
| `https://coffeeaddict.nz/api/beans.json` | Every bag we track, cheapest per 100g first. | beans[] of Bean. |
| `https://coffeeaddict.nz/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. |
| `https://coffeeaddict.nz/api/roasters.json` | The roasters we track, their shipping terms and where they roast. | roasters[] of Roaster. |
| `https://coffeeaddict.nz/api/schema.json` | JSON Schema for all three. | Draft 2020-12. |
| `https://coffeeaddict.nz/api/openapi.json` | The same, as OpenAPI 3.1. | For agents and MCP wrappers that read specs. |
| `https://coffeeaddict.nz/llms.txt` | What this site is, in the emerging convention. | Markdown, at the root. |

## Every payload carries

| Field | Meaning |
|---|---|
| `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. |

## Bean

One bag, in /api/beans.json and in every list in /api/deals.json.

| Field | Type | Meaning |
|---|---|---|
| `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. |

## Shipping

The `shipping` object on a Bean: what it costs to get THAT bag to a door.

| Field | Type | Meaning |
|---|---|---|
| `published` | boolean | Whether the roaster publishes a rate outside checkout. Four of thirteen do not. |
| `free_over_nzd` | number\|null | Order total they ship free at, where they publish one. |
| `flat_nzd` | number\|null | Their flat rate below that, where they publish one. |
| `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 their free-shipping threshold. |
| `delivered_price_nzd` | number\|null | This bag at the door: price plus shipping. |
| `delivered_price_exact` | boolean | 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` | 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. |

## Roaster

One roaster, in /api/roasters.json.

| Field | Type | Meaning |
|---|---|---|
| `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`. |

## Terms

The `shipping` object on a Roaster: what they publish, not what a bag costs.

| Field | Type | Meaning |
|---|---|---|
| `published` | boolean | Whether they publish a rate outside checkout. Four of thirteen do not. |
| `free_over_nzd` | number\|null | Order total they ship free at, where they publish one. |
| `flat_nzd` | number\|null | Their flat rate below that, where they publish one. |
| `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. |

## What we will not tell you

- A price we could not read is null. We have never once guessed one.
- A delivered price with delivered_price_exact: false is a FLOOR — the roaster publishes no rate outside checkout, so the real number is that or more. Four of thirteen are like this.
- last_price_change: null means the price has not moved since we started watching. It does not mean we have no history for the bag.
- first_seen is when WE first saw a bag. On the day we started, every bag was first seen — which is why nothing from that first collection is ever reported as an arrival.
- flavour_notes are the roaster's own words, verbatim. flavours are those words mapped onto our own eighteen-key vocabulary, and that mapping is ours, not theirs.
- Nothing here is sponsored and no roaster has ever paid for a position. Links to shops go through /go/ so we can count how many readers we send each roaster. We earn nothing from them — no affiliate deals, no commission, no paid placement — and if that ever changes this line changes with it. The link to publish is `url`; `shop_url` is where it lands, so the destination is never hidden from you.

## About this data

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.

One scrape a night at 15:40 UTC (about 03:40 in New Zealand), then the whole site and every endpoint is rebuilt.

Links to shops go through /go/ so we can count how many readers we send each roaster. We earn nothing from them — no affiliate deals, no commission, no paid placement — and if that ever changes this line changes with it.

Cite as: Coffee Addict — every specialty coffee bean on sale in New Zealand, priced per 100g and re-checked every night. https://coffeeaddict.nz

Machine-readable: `https://coffeeaddict.nz/api/beans.json`, `https://coffeeaddict.nz/api/deals.json`, `https://coffeeaddict.nz/api/roasters.json`. Schema and terms: https://coffeeaddict.nz/api/. Everything this site publishes for agents: https://coffeeaddict.nz/llms.txt
