API Documentation

Access coffee and roaster data via our REST API. Use it to build apps, chatbots, or integrations.

Getting started

  1. Sign up or log in.
  2. Go to Dashboard → Developer and create an API key. Copy it immediately — you won't see it again.
  3. Send requests to https://www.indiancoffeebeans.com/api/v1 with the key in the Authorization header.

Authentication

Include your API key on every request (except GET /health). Use either header:

Authorization: Bearer icb_live_<your-key>
# or
X-API-Key: icb_live_<your-key>

Missing or invalid keys receive 401 Unauthorized:

{ "error": "Invalid or missing API key" }

Endpoints

Base URL: https://www.indiancoffeebeans.com/api/v1. All responses are JSON.

GET /health(no auth)

Service health check. Use this to verify the API is reachable. No API key required.

Response 200

{
  "status": "ok",
  "timestamp": "2026-02-26T10:00:00.000Z",
  "version": "1.0.0",
  "environment": "production"
}
GET /coffees

Returns a paginated list of coffees with optional filters and sorting. Use this to power search, discovery, or recommendation flows.

Query parameters

  • page (number) — Page number, default 1
  • limit (number) — Items per page, default 15
  • sort — One of: price_asc, price_desc, newest, best_value, rating_desc, name_asc. Default: relevance
  • q (string) — Full-text search on name and description
  • roastLevels — Comma-separated: light, light_medium, medium, medium_dark, dark
  • processes — Comma-separated: washed, natural, honey, etc.
  • regions — Comma-separated region slugs
  • roasters — Comma-separated roaster slugs
  • flavors — Comma-separated flavor slugs
  • inStockOnly — Set to 1 to only return in-stock coffees
  • minPrice, maxPrice — Numeric price filters (INR)
  • decafOnly, worksWithMilk — Set to 1 for true

Response 200

name is the raw name as listed by the roaster and is kept stable for matching. display_name is the cleaned version we render (HTML entities decoded, marketing and pack-size suffixes removed, consistent casing). Prefer display_name for display and fall back to name if it is null.

{
  "items": [
    {
      "coffee_id": "uuid",
      "slug": "example-single-origin",
      "name": "Example Single Origin",
      "display_name": "Example Single Origin",
      "roaster_id": "uuid",
      "roaster_slug": "blue-tokai",
      "roaster_name": "Blue Tokai",
      "hq_city": "Mumbai",
      "hq_country": "India",
      "process": "washed",
      "roast_level": "medium",
      "rating_avg": 4.2,
      "rating_count": 15,
      "min_price_in_stock": 450,
      "best_normalized_250g": 450,
      "in_stock_count": 2,
      "direct_buy_url": "https://...",
      "decaf": false,
      "is_limited": false,
      "tags": ["featured"],
      "flavor_keys": ["berry", "chocolate"]
    }
  ],
  "page": 1,
  "limit": 15,
  "total": 120,
  "totalPages": 8
}
GET /coffees/:slug

Returns a single coffee by its URL slug (e.g. example-single-origin). Includes full details: variants, images, flavor notes, regions, estates, brew methods, and embedded roaster.

Path parameters

  • slug (string) — Coffee slug from the list or website URL

Response 200

{
  "id": "uuid",
  "slug": "example-single-origin",
  "name": "Example Single Origin",
  "display_name": "Example Single Origin",
  "description_md": "Full markdown description...",
  "roaster": { "id": "uuid", "slug": "blue-tokai", "name": "Blue Tokai", "website": "https://..." },
  "variants": [
    { "id": "uuid", "weight_g": 250, "price_current": 450, "in_stock": true, "grind": "filter" }
  ],
  "images": [{ "url": "https://...", "alt": "..." }],
  "flavor_notes": [{ "descriptor": "Berry", "family": "Fruity" }],
  "regions": [{ "region_id": "uuid", "display_name": "Chikmagalur", "pct": 100 }],
  "rating_avg": 4.2,
  "rating_count": 15,
  "process": "washed",
  "roast_level": "medium",
  "summary": { "coffee_id": "uuid", "process": "washed", ... }
}

Response 404 if slug not found: { "error": "Coffee not found" }

GET /coffees/filter-meta

Returns available filter options and counts (e.g. roast levels, processes, regions) and total coffee count. Accepts the same query parameters as GET /coffees so counts can be scoped to the current filters.

Response 200 (simplified)

{
  "totals": { "coffees": 120 },
  "roast_levels": [{ "value": "medium", "label": "Medium", "count": 45 }],
  "processes": [{ "value": "washed", "label": "Washed", "count": 60 }],
  "regions": [...],
  "flavors": [...]
}
GET /roasters

Returns a paginated list of roasters with optional filters and sorting.

Query parameters

  • page, limit — Pagination (default 1, 15)
  • sort — One of: relevance, name_asc, name_desc, coffee_count_desc, rating_desc, newest
  • q — Text search on roaster name
  • cities, states, countries — Comma-separated values
  • activeOnly — Set to 1 for active roasters only

Response 200

{
  "items": [
    {
      "id": "uuid",
      "slug": "blue-tokai",
      "name": "Blue Tokai",
      "website": "https://bluetokaicoffee.com",
      "hq_city": "Mumbai",
      "hq_state": "Maharashtra",
      "hq_country": "India",
      "is_active": true,
      "instagram_handle": "bluetokaicoffee",
      "coffee_count": 24,
      "avg_coffee_rating": 4.1,
      "rated_coffee_count": 18
    }
  ],
  "page": 1,
  "limit": 15,
  "total": 45,
  "totalPages": 3
}
GET /roasters/:slug

Returns a single roaster by slug with full profile and embedded list of their coffees.

Path parameters

  • slug (string) — Roaster slug (e.g. blue-tokai)

Response 200 (simplified)

{
  "id": "uuid",
  "slug": "blue-tokai",
  "name": "Blue Tokai",
  "description": "Roaster bio...",
  "website": "https://...",
  "logo_url": "https://...",
  "hq_city": "Mumbai",
  "hq_country": "India",
  "avg_rating": 4.2,
  "total_ratings_count": 120,
  "coffees": [ /* array of CoffeeSummary */ ]
}

Response 404: { "error": "Roaster not found" }

GET /usage

Returns usage statistics for the API key used in the request: today's request count, hourly breakdown for today, and daily totals for the past days (from Redis).

Response 200

{
  "todayTotal": 42,
  "hourlyToday": [
    { "hour": "00", "count": 0 },
    { "hour": "09", "count": 12 },
    ...
  ],
  "dailyTotals": [
    { "date": "20260225", "count": 100 },
    { "date": "20260224", "count": 85 }
  ]
}
POST /users(Phase 2)

Register an external user and get a stable anon_id (UUID) to use when submitting reviews. Call this once per user in your system; store the returned anon_id and send it with POST /reviews so multiple reviews from the same user are attributed correctly.

Request body (JSON)

  • external_user_id (string, required) — Your internal user ID (e.g. from your auth). Stored hashed; same ID always returns the same anon_id.
  • display_name (string, optional) — Not stored currently; reserved for future use.
{
  "external_user_id": "usr_abc123",
  "display_name": "Optional"
}

Response 200

{
  "anon_id": "550e8400-e29b-41d4-a716-446655440000"
}
POST /reviews(Phase 2)

Submit a review for a coffee or roaster on behalf of one of your users. You must provide either anon_id (from POST /users) or external_user_id; if you send external_user_id, an identity is created or looked up automatically. Reviews are stored with status pending_external for moderation; entity rating aggregates update via existing triggers.

Request body (JSON)

  • entity_type (string, required) — "coffee" or "roaster"
  • entity_id (string, required) — UUID of the coffee or roaster
  • rating (number, optional) — 1–5
  • recommend (boolean, optional)
  • value_for_money, works_with_milk (boolean, optional)
  • brew_method (string, optional) — One of: whole, filter, espresso, drip, other, turkish, moka_pot, cold_brew, aeropress, channi
  • comment (string, optional) — Max 5000 characters
  • anon_id (string, optional) — UUID from POST /users. Omit if using external_user_id.
  • external_user_id (string, optional) — Your user ID; identity is created or resolved. Omit if using anon_id.

At least one of: rating, recommend, value_for_money, works_with_milk, or comment is required.

{
  "entity_type": "coffee",
  "entity_id": "550e8400-e29b-41d4-a716-446655440000",
  "rating": 4,
  "recommend": true,
  "value_for_money": true,
  "works_with_milk": false,
  "brew_method": "filter",
  "comment": "Great single origin.",
  "external_user_id": "usr_abc123"
}

Response 200

{
  "id": "uuid-of-created-review"
}

Responses 400 for validation errors (e.g. missing entity_id, invalid rating, or missing both anon_id and external_user_id).

Rate limits

Default: 60 requests per minute per key (sliding window). When exceeded you receive 429 Too Many Requests with a Retry-After header (seconds until reset):

{ "error": "Rate limit exceeded", "retry_after": 45 }

Errors

All error responses use a JSON body with an error string. Common status codes:

  • 400 — Bad request (invalid params or body)
  • 401 — Invalid or missing API key
  • 404 — Resource not found (e.g. coffee or roaster slug)
  • 429 — Rate limit exceeded; check Retry-After header
  • 500 — Server error; error may contain a message

Code examples

JavaScript (fetch)

const res = await fetch(
  `https://www.indiancoffeebeans.com/api/v1/coffees?limit=5&roastLevels=medium&sort=rating_desc`,
  {
    headers: {
      Authorization: `Bearer ${process.env.ICB_API_KEY}`,
    },
  }
);
const data = await res.json();
if (!res.ok) throw new Error(data.error || res.statusText);
console.log(data.items); // coffee list

Python (requests)

import os
import requests

resp = requests.get(
    "https://www.indiancoffeebeans.com/api/v1/coffees",
    params={"limit": 5, "sort": "rating_desc"},
    headers={"Authorization": f"Bearer {os.environ['ICB_API_KEY']}"},
)
resp.raise_for_status()
data = resp.json()
print(data["items"])

curl

curl -H "Authorization: Bearer icb_live_YOUR_KEY" \
  "https://www.indiancoffeebeans.com/api/v1/coffees?limit=5"
curl -H "Authorization: Bearer icb_live_YOUR_KEY" \
  "https://www.indiancoffeebeans.com/api/v1/coffees/example-single-origin"

Questions? Email support@indiancoffeebeans.com.