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 15sort— 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
{
"items": [
{
"coffee_id": "uuid",
"slug": "example-single-origin",
"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",
"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)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
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-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.