API Documentation
Access coffee and roaster data via our REST API. Use it to build apps, chatbots, or integrations.
Getting started
- Sign up or log in.
- Go to Dashboard → Developer and create an API key. Copy it immediately — you won't see it again.
- Send requests to
https://www.indiancoffeebeans.com/api/v1with the key in theAuthorizationheader.
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.
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"
}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 1limit(number) — Items per page, default 15, max 100sort— One of:price_asc,price_desc,newest,best_value,rating_desc,name_asc. Default: relevanceq(string) — Full-text search on name and descriptionroastLevels— Comma-separated: light, light_medium, medium, medium_dark, darkprocesses— Comma-separated: washed, natural, honey, etc.regions— Comma-separated region slugsroasters— Comma-separated roaster slugsflavors— Comma-separated flavor slugsinStockOnly— Set to1to only return in-stock coffeesminPrice,maxPrice— Numeric price filters (INR)decafOnly,worksWithMilk— Set to1for 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
}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" }
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": [...]
}Returns a paginated list of roasters with optional filters and sorting.
Query parameters
page,limit— Pagination (default 1, 15; limit max 100)sort— One of:relevance,name_asc,name_desc,coffee_count_desc,rating_desc,newestq— Text search on roaster namecities,states,countries— Comma-separated valuesactiveOnly— Set to1for 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
}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" }
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 }
]
}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 sameanon_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"
}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 roasterrating(number, optional) — 1–5recommend(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, channicomment(string, optional) — Max 5000 charactersanon_id(string, optional) — UUID fromPOST /users. Omit if usingexternal_user_id.external_user_id(string, optional) — Your user ID; identity is created or resolved. Omit if usinganon_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
Limits apply per account, shared across all your keys. The free plan allows 30 requests per minute (sliding window) and 5,000 requests per calendar month (UTC). Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Quota-Limit and X-Quota-Remaining headers.
Over the per-minute limit you receive 429 Too Many Requests with a Retry-After header (seconds until reset):
{ "error": "Rate limit exceeded", "retry_after": 45 }Over the monthly quota you receive 429 until the next period:
{ "error": "Monthly quota exceeded", "upgrade_url": "…/dashboard/developer" }The free and Pro plans are for development and internal tools. Using ICB data in a customer-facing product needs a Commercial licence; get in touch.
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 or monthly quota exceeded; check
Retry-Afterheader - 500 — Server error;
errormay 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 listPython (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.