Skip to main content
BrewMarkBREWMARK
API Reference

REST API Documentation

Complete reference for integrating with the BrewMark API. All endpoints return JSON. Base URL: https://brewmark.io

← Back to Developers
Authentication

Sessions & tokens

BrewMark supports two authentication methods. Web clients use HTTP-only cookie sessions set automatically after login. Mobile clients (iOS app) receive a JWT token in the login response body, which must be sent as a Bearer token in the Authorization header.

Mobile auth header
Authorization: Bearer <jwt-token>
Content-Type: application/json
POST/api/auth/register3/min per IP

Create a new user account with email and password. Returns a JWT token for mobile clients or sets a session cookie for web clients.

Request Body (JSON)
emailstringrequiredUser email address
passwordstringrequiredAccount password
Response
successbooleanWhether registration succeeded
userobject{ id, email, createdAt }
tokenstringJWT session token (mobile clients only)
Example
POST /api/auth/register
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword123"
}

→ 200 OK
{
  "success": true,
  "user": { "id": 1, "email": "user@example.com" },
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
POST/api/auth/login5/min per IP

Authenticate with email and password. Returns a JWT token for mobile clients. Web clients receive an HTTP-only session cookie.

Request Body (JSON)
emailstringrequiredUser email address
passwordstringrequiredAccount password
Response
successbooleanWhether login succeeded
userobject{ id, email, createdAt }
tokenstringJWT session token (mobile clients only)
Example
POST /api/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword123"
}

→ 200 OK
{
  "success": true,
  "user": { "id": 1, "email": "user@example.com" },
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
POST/api/auth/magic-link5/min per IP

Request a passwordless magic link sent to the user's email. Always returns success to prevent email enumeration.

Request Body (JSON)
emailstringrequiredEmail address to send the magic link to
Response
successbooleanAlways true (prevents email enumeration)
messagestringConfirmation message
POST/api/auth/apple5/min per IP

Sign in or register with Apple. For mobile clients, include the Apple identity token. Returns a JWT for subsequent authenticated requests.

Request Body (JSON)
identityTokenstringrequiredApple Sign In identity token (JWT from ASAuthorizationAppleIDCredential)
emailstringUser email (provided on first sign-in only)
Response
successbooleanWhether authentication succeeded
userobject{ id, email, name }
tokenstringJWT session token (mobile clients only)
Example
POST /api/auth/apple
Content-Type: application/json

{
  "identityToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

→ 200 OK
{
  "success": true,
  "user": { "id": "clx123...", "email": "user@icloud.com" },
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
GET/api/auth/meRequired

Get the currently authenticated user's profile and preferences. Use this to check if the session is valid and to load the user's taste profile.

Response
userobject{ id, email, name, profile: { acidityLevel, bodyLevel, sweetnessLevel, ... } }
Example
GET /api/auth/me
Authorization: Bearer <token>

→ 200 OK
{
  "user": {
    "id": "clx123...",
    "email": "user@icloud.com",
    "name": "Jeff",
    "profile": {
      "acidityLevel": 3,
      "bodyLevel": 4,
      "sweetnessLevel": 3,
      "bitternessLevel": 2,
      "flavorComplexityLevel": 4,
      "aftertasteLevel": 3
    }
  }
}
POST/api/auth/logoutRequired

End the current session. Clears the session cookie for web clients. Mobile clients should discard the stored JWT.

Response
successbooleanAlways true on success
Recipe Adaptation

Personalized recipes

The core product endpoints. These adapt brew recipes to the user's equipment and taste preferences using grind index normalization, taste vector matching, and Wilson score ranking.

POST/api/adaptRecipeOptional

Calculate an adapted brew recipe for a specific coffee, grinder, and machine combination. Optionally adjusts for taste preferences (strength, taste direction, grind nudge).

Request Body (JSON)
machineIdnumberrequiredBrewer/machine ID
grinderIdnumberrequiredGrinder ID
coffeeProfileIdnumberrequiredCoffee profile ID
userIdstringUser ID (for taste profile matching)
strengthnumberStrength preference 1-5 (default: 3)
tastenumberTaste direction -2 to +2 (negative=brighter, positive=richer)
grindNudgeStepsnumberManual grind adjustment in steps (-5 to +5)
Response
baseRecipeobjectOriginal recipe before adaptation
adaptedRecipeobject{ doseGrams, waterGrams, ratio, grindSetting, grindIndex, waterTempF, brewTimeSeconds }
recommendedVariantobjectBest-matching variant for user's taste profile
otherVariantsarrayAlternative recipe variants ranked by relevance
Example
POST /api/adaptRecipe
Content-Type: application/json

{
  "machineId": 1,
  "grinderId": 5,
  "coffeeProfileId": 12,
  "strength": 4,
  "taste": 1
}

→ 200 OK
{
  "baseRecipe": {
    "method": "V60",
    "doseGrams": 18,
    "waterGrams": 300,
    "ratio": 16.67
  },
  "adaptedRecipe": {
    "doseGrams": 20,
    "waterGrams": 300,
    "ratio": 15,
    "grindSetting": "14 clicks",
    "grindIndex": 42,
    "waterTempF": 205,
    "brewTimeSeconds": 210
  },
  "recommendedVariant": { ... },
  "otherVariants": [ ... ]
}
POST/api/get-recipeOptional

Get an adapted starting recipe for a specific coffee + equipment combo. Returns the best starting point with ranked community variants.

Request Body (JSON)
coffeeIdnumberrequiredCoffee profile ID
grinderIdnumberrequiredGrinder ID
machineIdnumberrequiredMachine/brewer ID
filterIdnumberFilter ID (optional)
volumeGramsnumberrequiredTarget water volume in grams
Response
startingPointobject|nullAdapted base recipe for the equipment combo
recommendationobject|nullBest-matching community recipe for user taste
variantsarrayRanked community recipe variants
coffeeobjectCoffee profile details
equipmentobjectEquipment details used for adaptation
Example
POST /api/get-recipe
Content-Type: application/json

{
  "coffeeId": 12,
  "grinderId": 5,
  "machineId": 1,
  "volumeGrams": 300
}

→ 200 OK
{
  "startingPoint": {
    "doseGrams": 18,
    "waterGrams": 300,
    "ratio": 16.67,
    "grindSetting": "14 clicks",
    "grindIndex": 42,
    "waterTempF": 205,
    "brewTimeSeconds": 210
  },
  "recommendation": { ... },
  "variants": [ ... ]
}
GET/api/recipesOptional

List and search brew recipes. Returns paginated results with recipe details. Authenticated users get taste match scores.

Query Parameters
qstringSearch by recipe name or coffee
grinderIdnumberFilter by grinder
machineIdnumberFilter by machine/brewer
roastLevelstringFilter: Light, Medium, Dark
limitnumberResults per page (default: 20)
cursorstringPagination cursor
Response
dataarrayArray of recipe objects
totalnumberTotal matching recipes
cursorstring|nullNext page cursor
hasMorebooleanWhether more results exist
GET/api/recipes/{id}

Get a single recipe with full details including coffee, grinder, machine, filter, and creator info. Supports ETag caching.

Response
idnumberRecipe ID
coffeeobjectCoffee profile with roaster details
grinderobjectGrinder used
machineobjectMachine/brewer used
createdByobjectRecipe author (display name only)
GET/api/recipes/{id}/comments

List all comments on a recipe.

Response
commentsarrayArray of comment objects with author info
POST/api/recipes/{id}/commentsRequired10/min per IP

Add a comment to a recipe. Sends an email notification to the recipe author.

Request Body (JSON)
bodystringrequiredComment text (1-2000 characters)
Response
commentobjectThe created comment object
DELETE/api/recipes/{id}/commentsRequired

Delete a comment. Only the comment author or recipe owner can delete.

Request Body (JSON)
commentIdnumberrequiredComment ID to delete
GET/api/browse/recipes

Browse recipes with filtering and offset-based pagination. Supports sorting by newest, highest rated, or most brewed.

Query Parameters
qstringSearch query
brewMethodstringFilter by brew method
roasterstringFilter by roaster name
roastLevelstringFilter by roast level
sortstringnewest, highest_rated, or most_brewed
pagenumberPage number (default: 1)
limitnumberResults per page (default: 20)
Response
dataarrayArray of recipe objects
totalnumberTotal matching recipes
totalPagesnumberTotal pages available
pagenumberCurrent page number
GET/api/recommendationsRequired

Get personalized recipe recommendations based on the user's 6D taste vector. Returns recipes ranked by taste match score.

Query Parameters
limitnumberMax results (default: 5, max: 20)
Response
recommendationsarrayRecipes ranked by taste match score
hasProfilebooleanWhether the user has a taste profile set
profileSummaryobject{ strengthPreference, roastPreference }
Coffees

Coffee catalog

Search and browse the coffee database. Each coffee has a roast level, origin, flavor notes, and taste profile used for recipe matching.

GET/api/coffees

Search and list coffees across all roasters. Supports fuzzy search by coffee name, roaster name, or origin.

Query Parameters
qstringSearch query (fuzzy matched against name, roaster, origin)
roasterstringFilter by roaster slug
roastLevelstringFilter: Light, Medium-Light, Medium, Medium-Dark, Dark
sortstringSort by: name, newest, popular (default: name)
limitnumberResults per page (default: 20)
cursorstringPagination cursor
Response
dataarrayArray of coffee profile objects
cursorstring|nullNext page cursor
hasMorebooleanWhether more results exist
Example
GET /api/coffees?q=ethiopian&roastLevel=Light&limit=5

→ 200 OK
{
  "data": [
    {
      "id": 12,
      "name": "Yirgacheffe Natural",
      "roasterName": "Onyx Coffee Lab",
      "roasterSlug": "onyx-coffee-lab",
      "roastLevel": "Light",
      "origin": "Ethiopia",
      "flavorProfile": "Blueberry, Jasmine, Honey",
      "acidityLevel": 4,
      "bodyLevel": 2,
      "processingMethod": "Natural"
    }
  ],
  "cursor": "eyJpZCI6MTJ9",
  "hasMore": true
}
POST/api/coffeesRequired

Submit a new coffee to the catalog. Requires authentication. The coffee is associated with a roaster by name (auto-matched or created).

Request Body (JSON)
namestringrequiredCoffee name
roasterNamestringrequiredRoaster name (matched to existing or creates placeholder)
roastLevelstringrequiredLight, Medium-Light, Medium, Medium-Dark, or Dark
aciditynumberAcidity level 1-5
bodynumberBody level 1-5
flavorProfilestringComma-separated flavor notes
originstringCountry or region of origin
processingMethodstringWashed, Natural, Honey, etc.
descriptionstringFree-text description
Response
coffeeobjectThe created coffee profile object
GET/api/coffees/{id}

Get a single coffee profile with full details including roaster, origin, flavor notes, and community recipe count. Supports ETag caching.

Response
coffeeobjectFull coffee profile with submittedBy user and _count.communityRecipes
Example
GET /api/coffees/12

→ 200 OK
{
  "coffee": {
    "id": 12,
    "name": "Yirgacheffe Natural",
    "roasterName": "Onyx Coffee Lab",
    "roastLevel": "Light",
    "origin": "Ethiopia",
    "processingMethod": "Natural",
    "flavorProfile": "Blueberry, Jasmine, Honey",
    "acidityLevel": 4,
    "bodyLevel": 2,
    "_count": { "communityRecipes": 8 }
  }
}
GET/api/coffees/{id}/stats

Get brewing statistics for a specific coffee. Includes recipe count, vote totals, top grinders/machines, and ratio/grind ranges.

Response
coffeeIdnumberCoffee profile ID
recipeCountnumberNumber of community recipes
votesobject{ total, upvotes, downvotes }
topGrindersarrayMost-used grinders with avg grind index
topMachinesarrayMost-used machines/brewers
ratioRangeobject|null{ min, max, avg } water:coffee ratio
grindRangeobject|null{ min, max, avg } grind index (0-100)
GET/api/coffees/suggestions

Get coffee name suggestions for autocomplete. Returns up to 5 fuzzy-matched coffees.

Query Parameters
namestringCoffee name to suggest for
roasterstringRoaster name to suggest for
Response
suggestionsarrayArray of matching coffee profiles
Equipment

Grinders, machines & filters

Equipment endpoints return the full catalog of supported grinders, brewers, and filters. Grinder data includes the grind index mapping used for recipe adaptation.

GET/api/grinders

List all supported grinders. Each grinder includes its brand, model, grind step count, and grind index range for recipe adaptation.

Query Parameters
brandstringFilter by brand name
Response
grindersarrayAll grinder objects with id, brand, model, minGrindIndex, maxGrindIndex, clicksPerFullRange
byBrandobjectGrinders grouped by brand name
brandsarrayList of unique brand names
Example
GET /api/grinders?brand=Comandante

→ 200 OK
{
  "grinders": [
    {
      "id": 5,
      "brand": "Comandante",
      "model": "C40 MK4",
      "minGrindIndex": 0,
      "maxGrindIndex": 100,
      "clicksPerFullRange": 40
    }
  ],
  "brands": ["Comandante"]
}
GET/api/machines

List all supported brewing machines and manual brewers (V60, AeroPress, Chemex, espresso machines, etc.).

Query Parameters
brandstringFilter by brand
brewMethodstringFilter by brew method type
Response
machinesarrayAll machine/brewer objects with id, brand, model, brewMethod, defaultWaterTempF
byBrandobjectMachines grouped by brand
brandsarrayList of unique brand names
GET/api/filters

List all filter types (paper, metal, cloth) with their attributes.

Response
filtersarrayArray of { id, name, type, description }
GET/api/brew-methods

List all available brew methods (V60, AeroPress, Chemex, French Press, Espresso, etc.).

Response
dataarrayArray of brew method objects
Roasters

Roaster profiles

Browse and search roaster profiles. Each roaster has a public page with their coffee catalog and community recipes.

GET/api/roasters

List all roasters with search and pagination.

Query Parameters
searchstringSearch by roaster name
pagenumberPage number (default: 1)
limitnumberResults per page (default: 20)
Response
roastersarrayArray of roaster objects
paginationobject{ page, limit, total, totalPages }
Example
GET /api/roasters?search=onyx&limit=5

→ 200 OK
{
  "roasters": [
    {
      "id": 1,
      "name": "Onyx Coffee Lab",
      "slug": "onyx-coffee-lab",
      "location": "Rogers, AR",
      "description": "Specialty roaster...",
      "logoUrl": "https://...",
      "isVerified": true,
      "_count": { "coffees": 24 }
    }
  ],
  "pagination": { "page": 1, "limit": 5, "total": 1, "totalPages": 1 }
}
GET/api/roasters/{slug}

Get a single roaster profile by slug. Includes coffee count and community recipe count. Supports ETag caching.

Response
idnumberRoaster ID
namestringRoaster name
slugstringURL-safe slug
taglinestringRoaster tagline
stylestringRoasting style
websitestringRoaster website URL
_countobject{ coffees: number }
recipeCountnumberTotal community recipes for this roaster
GET/api/roasters/{slug}/coffees

List a roaster's coffee catalog with optional roast level filtering. Cursor-based pagination sorted alphabetically.

Query Parameters
roastLevelstringFilter by roast level
cursorstringPagination cursor
limitnumberResults per page (default: 20)
Response
dataarrayCoffee profiles with recipe counts
cursorstring|nullNext page cursor
hasMorebooleanWhether more results exist
GET/api/roasters/{slug}/recipes

List community-submitted recipes for all of a roaster's coffees. Cursor-based pagination.

Query Parameters
cursorstringPagination cursor
limitnumberResults per page (default: 20)
Response
dataarrayCommunity recipe objects with coffee, grinder, and brewer details
cursorstring|nullNext page cursor
hasMorebooleanWhether more results exist
Community Recipes

User-submitted recipes & voting

Community recipes are submitted by users and ranked using Wilson score confidence intervals based on upvotes and downvotes.

GET/api/community-recipesOptional

List community recipes with optional filters. Authenticated users get taste match scores. Cursor-based pagination.

Query Parameters
coffeeIdnumberFilter by coffee
grinderIdnumberFilter by grinder
machineIdnumberFilter by machine
createdByMebooleanOnly show user's own recipes
limitnumberResults per page (default: 20)
cursorstringPagination cursor
Response
dataarrayCommunity recipe objects with taste match scores
totalnumberTotal matching recipes
cursorstring|nullNext page cursor
hasMorebooleanWhether more results exist
POST/api/community-recipesRequired

Submit a community brew recipe. Requires authentication. The recipe is public and can be voted on by other users.

Request Body (JSON)
coffeeProfileIdnumberrequiredCoffee this recipe is for
grinderIdnumberrequiredGrinder used
machineIdnumberrequiredMachine/brewer used
grindSettingstringrequiredGrind setting (e.g. "14 clicks")
doseGramsnumberrequiredCoffee dose in grams
waterGramsnumberrequiredWater amount in grams
rationumberrequiredWater-to-coffee ratio
filterIdnumberFilter used (optional)
brewTimeSecondsnumberTotal brew time in seconds
waterTempFnumberWater temperature in Fahrenheit
notesstringBrewing notes or instructions
tasteProfileobject{ acidity, body, sweetness, bitterness } ratings 1-5
POST/api/community-recipes/{id}/voteRequired30/min per IP

Vote on a community recipe. Each user can have one vote per recipe. Voting again changes the vote.

Request Body (JSON)
vote"UP" | "DOWN"requiredVote direction
Response
votestringThe recorded vote (UP or DOWN)
messagestringConfirmation message
GET/api/community-recipes/{id}/voteOptional

Get the current user's vote on a specific community recipe. Returns null if the user hasn't voted.

Response
vote"UP" | "DOWN" | nullCurrent user's vote, or null
GET/api/community-recipes/{id}/brew-logOptional

Get aggregated brew ratings for a community recipe. If authenticated, also returns the user's own rating.

Response
avgnumberAverage rating (1-5)
countnumberTotal ratings count
userRatingobject{ rating, notes } — current user's rating (auth only)
POST/api/community-recipes/{id}/brew-logRequired30/min per IP

Log a brew and rate a community recipe. Creates a brew log entry and upserts the rating. Notifies the recipe author.

Request Body (JSON)
ratingnumberrequiredRating 1-5
notesstringBrew notes
doseGramsnumberCoffee dose used
grindSettingstringGrind setting used
Response
brewLogobject{ id } — created brew log
ratingobject{ avg, count } — updated aggregate rating
User Profile

Taste profile & preferences

The user's taste profile is a 6-dimensional vector used for recipe matching. Each dimension is an integer from 1 to 5.

GET/api/user/profileRequired

Get the current user's taste profile and preferences.

Response
profileobject{ acidityLevel, bodyLevel, sweetnessLevel, bitternessLevel, flavorComplexityLevel, aftertasteLevel } — each 1-5
PUT/api/user/profileRequired

Update the user's taste profile. Send only the fields you want to change.

Request Body (JSON)
acidityLevelnumber1 (low) to 5 (high)
bodyLevelnumber1 (light) to 5 (full)
sweetnessLevelnumber1 (dry) to 5 (sweet)
bitternessLevelnumber1 (low) to 5 (high)
flavorComplexityLevelnumber1 (simple) to 5 (complex)
aftertasteLevelnumber1 (short) to 5 (lingering)
Response
profileobjectUpdated taste profile object
POST/api/user/profile/onboardingRequired

Complete the taste profile onboarding flow. Sets all taste dimensions at once.

Request Body (JSON)
acidityLevelnumberrequired1-5
bodyLevelnumberrequired1-5
sweetnessLevelnumberrequired1-5
bitternessLevelnumberrequired1-5
flavorComplexityLevelnumberrequired1-5
aftertasteLevelnumberrequired1-5
GET/api/user/saved-recipesRequired

Get the user's bookmarked community recipes.

Response
savedRecipesarrayArray of saved recipe objects with full recipe details
POST/api/user/saved-recipesRequired

Bookmark a community recipe.

Request Body (JSON)
communityRecipeIdnumberrequiredCommunity recipe ID to save
DELETE/api/user/saved-recipesRequired

Remove a bookmarked community recipe.

Request Body (JSON)
communityRecipeIdnumberrequiredCommunity recipe ID to unsave
GET/api/users/taste-profileRequired

Get the user's 6-dimensional taste vector used for recipe matching. Returns defaults (all 0.5) if no profile set.

Response
profileobject{ strengthPreference, acidityTolerance, roastPreference, bodyVsClarity, bitternessSensitivity, explorerType } — all 0-1 scale
isDefaultbooleanWhether using default values (no profile set yet)
POST/api/users/taste-profileRequired

Set or update the user's taste profile. Accepts partial updates — only send fields you want to change.

Request Body (JSON)
strengthPreferencenumber0 (light) to 1 (strong)
acidityTolerancenumber0 (low acid) to 1 (high acid)
roastPreferencenumber0 (light roast) to 1 (dark roast)
bodyVsClaritynumber0 (clarity) to 1 (body)
bitternessSensitivitynumber0 (tolerant) to 1 (sensitive)
explorerTypenumber0 (consistent) to 1 (adventurous)
Response
profileobjectUpdated taste profile
isDefaultbooleanAlways false after setting
GET/api/user/notification-preferencesRequired

Get the user's email notification settings.

Response
preferencesobject{ notifyRecipeComments, notifyRecipeFavorites, notifyRecipeBrews } — all boolean
PATCH/api/user/notification-preferencesRequired

Update notification preferences. Send only the fields you want to change.

Request Body (JSON)
notifyRecipeCommentsbooleanEmail when someone comments on your recipe
notifyRecipeFavoritesbooleanEmail when someone saves your recipe
notifyRecipeBrewsbooleanEmail when someone brews your recipe
GET/api/users/{id}

Get a public user profile with brewing statistics. Email is partially masked for privacy.

Response
userobject{ id, displayName, memberSince }
statsobject{ brewCount, savedCount, commentCount }
brewLogsarrayRecent brew logs (max 20)
savedRecipesarrayRecently saved recipes (max 20)
commentsarrayRecent comments (max 20)
Brew Logs

Brew session tracking

Log and track individual brew sessions. Includes timing, equipment, grind settings, and extraction feedback for improving future recipes.

GET/api/brew-logsRequired

List the user's brew logs. Supports filtering and cursor-based pagination.

Query Parameters
coffeeProfileIdnumberFilter by coffee
ratingnumberFilter by minimum rating (1-5)
savedAsRecipebooleanOnly logs saved as personal recipes
limitnumberResults per page (default: 20)
cursorstringPagination cursor
Response
dataarrayArray of brew log objects
totalnumberTotal matching logs
cursorstring|nullNext page cursor
hasMorebooleanWhether more results exist
POST/api/brew-logsRequired

Log a new brew session. Captures the full context of the brew for tracking and recipe improvement.

Request Body (JSON)
methodstringrequiredBrew method (V60, AeroPress, etc.)
coffeeGramsnumberrequiredCoffee dose in grams
waterGramsnumberrequiredWater amount in grams
grindSettingstringrequiredGrind setting used
coffeeProfileIdnumberCoffee profile ID
grinderIdnumberGrinder ID
machineIdnumberMachine/brewer ID
waterTempFnumberWater temperature (Fahrenheit)
brewTimeSecondsnumberTotal brew time in seconds
daysOffRoastnumberDays since roast date
outcomestringBrew outcome: good, over-extracted, under-extracted, etc.
notesstringFree-text notes
extractionFeedbackobject{ sour, bitter, watery, harsh, balanced } boolean indicators
Response
brewLogobjectThe created brew log
GET/api/brew-logs/{id}Required

Get a single brew log with full details including grinder info. Only accessible by the brew log owner.

Response
brewLogobjectFull brew log with grinder details
PATCH/api/brew-logs/{id}Required

Update a brew log. Use this to add ratings, taste notes, or outcome feedback after the brew.

Request Body (JSON)
outcomestringUpdated outcome assessment
ratingnumberRating 1-5
notesstringUpdated notes
tasteProfileobject{ acidity, body, sweetness, bitterness } ratings
extractionFeedbackobjectUpdated extraction feedback
DELETE/api/brew-logs/{id}Required

Delete a brew log.

GET/api/brew-logs/lastRequired

Get the user's most recent brew log. Useful for pre-filling the brew form with last-used settings.

GET/api/brew-logs/weekly-statsRequired

Get the user's weekly brewing statistics with trends, insights, and brew-by-day breakdown.

Response
statsobject{ totalBrews, totalCoffeeGrams, averageRating, streak, brewsToday, uniqueCoffees, allTimeBrews }
bestBrewobject|nullHighest-rated brew this week
insightsarrayUp to 2 insights (grind_sweet_spot, ratio_preference, streak_milestone, etc.)
brewsByDayarray{ label, date, brews[] } grouped by day
lastWeekobjectPrevious week stats for trend comparison
GET/api/brew-logs/{id}/compareRequired

Compare a brew log with the user's previous brew. Shows deltas for ratio, grind, water, and brew time.

Response
comparisonobject{ currentBrew, previousBrew, deltas: { ratio, grind, water, brewTime } }
POST/api/brew-logs/{id}/save-as-recipeRequired

Mark a brew log as a saved personal recipe. No request body needed.

Response
brewLogobjectUpdated brew log with savedAsRecipe: true
User Equipment & Library

Saved equipment & coffee library

Users can save their owned equipment and favorite coffees for quick access during recipe adaptation.

GET/api/user/grindersRequired

Get the user's saved grinders collection.

Response
grindersarrayUser grinder objects with grinder details and isDefault flag
POST/api/user/grindersRequired

Add a grinder to the user's collection. Upserts if already present.

Request Body (JSON)
grinderIdnumberrequiredGrinder ID from the catalog
isDefaultbooleanSet as the default grinder
nicknamestringCustom nickname for this grinder
DELETE/api/user/grindersRequired

Remove a grinder from the user's collection.

Request Body (JSON)
grinderIdnumberrequiredGrinder ID to remove
POST/api/user/grinders/calibrateRequired

Submit grind calibration data. Map your grinder settings to the universal grind index (0-100) for accurate recipe adaptation.

Request Body (JSON)
grinderIdnumberrequiredGrinder ID to calibrate
referencesarrayrequired[{ grindIndex: 0-100, setting: number, feedback: "TOO_FINE"|"JUST_RIGHT"|"TOO_COARSE" }]
Response
calibrationobjectSaved calibration data
GET/api/user/machinesRequired

Get the user's saved machines/brewers collection.

Response
machinesarrayUser machine objects with details and isDefault flag
POST/api/user/machinesRequired

Add a machine/brewer to the user's collection.

Request Body (JSON)
machineIdnumberrequiredMachine ID from the catalog
isDefaultbooleanSet as the default machine
nicknamestringCustom nickname
DELETE/api/user/machinesRequired

Remove a machine from the user's collection.

Request Body (JSON)
machineIdnumberrequiredMachine ID to remove
GET/api/user/filtersRequired

Get the user's saved filter preferences.

Response
filtersarrayUser filter objects with isDefault flag
POST/api/user/filtersRequired

Add a filter to the user's saved preferences.

Request Body (JSON)
filterIdnumberrequiredFilter ID from the catalog
isDefaultbooleanSet as default filter
DELETE/api/user/filtersRequired

Remove a filter from the user's saved preferences.

Request Body (JSON)
filterIdnumberrequiredFilter ID to remove
GET/api/user/coffee-libraryRequired

Get the user's coffee library with brew stats (total brews and last brewed date per coffee).

Response
libraryarrayLibrary entries with coffee details, personalNotes, rating, brew stats
POST/api/user/coffee-libraryRequired

Add a coffee to the user's library.

Request Body (JSON)
coffeeProfileIdnumberrequiredCoffee profile ID
personalNotesstringPersonal tasting notes
ratingnumberRating 1-5
DELETE/api/user/coffee-libraryRequired

Remove a coffee from the user's library.

Request Body (JSON)
coffeeProfileIdnumberrequiredCoffee profile ID to remove
GET/api/user/recipesRequired

Get the user's personal saved recipes with pagination.

Query Parameters
limitnumberResults per page (default: 50, max: 100)
pagenumberPage number (default: 1)
Response
recipesarrayUser recipe objects
POST/api/user/recipesRequired

Create a personal recipe.

Request Body (JSON)
methodstringrequiredBrew method
grindSettingnumberrequiredGrind setting used
rationumberrequiredWater:coffee ratio
doseGramsnumberrequiredCoffee dose in grams
waterGramsnumberrequiredWater amount in grams
grinderIdnumberGrinder ID
coffeeProfileIdnumberCoffee profile ID
namestringRecipe name
notesstringBrewing notes
PATCH/api/user/recipes/{id}Required

Update a personal recipe. Send only fields to change.

Request Body (JSON)
namestringRecipe name
notesstringUpdated notes
ratingnumberRating 1-5
grindSettingnumberGrind setting
rationumberUpdated ratio
DELETE/api/user/recipes/{id}Required

Delete a personal recipe.

Collections

Recipe collections

Users can organize saved community recipes into named collections for quick access by brew method, coffee type, or any personal taxonomy.

GET/api/collectionsRequired

List all of the user's recipe collections with item counts.

Response
collectionsarrayCollections with _count.items
POST/api/collectionsRequired

Create a new recipe collection.

Request Body (JSON)
namestringrequiredCollection name
descriptionstringCollection description
Response
collectionobjectCreated collection object
GET/api/collections/{id}Required

Get a collection with all its items. Includes full recipe details (coffee, grinder, machine).

Response
collectionobjectCollection with nested items array and full recipe details
PATCH/api/collections/{id}Required

Update a collection's name or description.

Request Body (JSON)
namestringUpdated name
descriptionstringUpdated description
DELETE/api/collections/{id}Required

Delete a collection and all its item associations.

POST/api/collections/{id}/itemsRequired

Add a recipe to a collection. Returns 409 if already present.

Request Body (JSON)
recipeIdnumberrequiredCommunity recipe ID to add
Response
itemobjectCreated collection item with recipe details
DELETE/api/collections/{id}/itemsRequired

Remove a recipe from a collection.

Request Body (JSON)
recipeIdnumberrequiredRecipe ID to remove
Discover & Search

Discovery endpoints

High-level discovery endpoints that combine system recommendations with community recipes, adapted for the user's equipment.

GET/api/discover

Get recipe suggestions for a specific grinder and optional filters. Returns one system recommendation plus up to 20 community recipes, all adapted to the user's grinder.

Query Parameters
grinderIdnumberrequiredGrinder ID (used for grind adaptation)
roaststringFilter by roast level: LIGHT, MEDIUM, DARK
coffeeIdnumberFilter by specific coffee
machineIdnumberFilter by machine/brewer
brewStrengthstringLIGHT, MEDIUM, or STRONG
Response
recipesarrayMixed array: 1 system recommendation (id: 0) + community recipes
totalnumberTotal results returned
grinderobject{ id, brand, name } — the grinder used for adaptation
Example
GET /api/discover?grinderId=5&roast=LIGHT&limit=10

→ 200 OK
{
  "recipes": [
    {
      "id": 0,
      "type": "recommended",
      "grindSetting": "22 clicks",
      "grindIndex": 55,
      "ratio": 16.67,
      "doseGrams": 18,
      "waterGrams": 300,
      "isAdapted": true
    },
    {
      "id": 42,
      "type": "community",
      "grindSetting": "20 clicks",
      "ratio": 15.5,
      "upvotes": 12,
      "avgRating": 4.3,
      "coffee": { "name": "Yirgacheffe", "roasterName": "Onyx" }
    }
  ],
  "total": 11,
  "grinder": { "id": 5, "brand": "Comandante", "name": "C40 MK4" }
}
GET/api/health

System health check. Returns database status, migration info, and latency metrics. No authentication required.

Response
statusstring"healthy", "degraded" (>500ms latency), or "unhealthy"
timestampstringISO 8601 timestamp
checksobject{ database: { status, latencyMs }, migrations: { status, applied } }
Embeds

Embeddable recipe data

These endpoints power the embed widget and are fully CORS-enabled. See the Embed Widget Documentation for the JavaScript widget integration guide.

All embed endpoints are cached for 5 minutes at the edge with a 10-minute stale-while-revalidate window.

GET/api/embed/{roasterSlug}/{coffeeSlug}

Fetch all brew recipes for a specific coffee. Returns roaster info, coffee details, and an array of recipes across brew methods.

Response
roasterobject{ name, slug }
coffeeobject{ name, slug, roastLevel, flavorNotes[] }
recipesarray{ method, doseGrams, waterGrams, ratio, waterTempF, grindSize, brewTimeSeconds, notes, isDefault }
POST/api/embed/track-click

Track when a user clicks through from an embedded recipe to BrewMark.

Request Body (JSON)
shareIdstringrequiredEmbed identifier (roaster-slug/coffee-slug or share ID)
POST/api/embed/track-method-switch

Track when a user switches brew methods within an embedded recipe card.

Request Body (JSON)
shareIdstringrequiredEmbed identifier
methodstringrequiredThe brew method selected
Technical Details

Rate limits

Per-endpoint limits (per IP)
POST /api/auth/register3 requests/min
POST /api/auth/login5 requests/min
POST /api/auth/apple5 requests/min
POST /api/auth/magic-link5 requests/min
GET /api/auth/magic-link/verify10 requests/min
POST /api/recipes/{id}/comments10 requests/min
POST /api/community-recipes/{id}/vote30 requests/min
POST /api/community-recipes/{id}/brew-log30 requests/min
POST /api/subscribe3 requests/min
POST /api/contact3 requests/min
POST /api/demo-requests3 requests/min
POST /api/tools/grind-feedback10 requests/min
All other endpointsNo hard limit (fair use)

When rate-limited, the API returns 429 Too Many Requests with a Retry-After header indicating seconds to wait before retrying.

Error Handling

Error responses

All error responses return a consistent JSON structure with an error field and the appropriate HTTP status code.

Error response format
{
  "error": "Human-readable error message"
}

// Common status codes:
// 400 — Bad Request (invalid parameters)
// 401 — Unauthorized (missing or expired token)
// 404 — Not Found
// 429 — Too Many Requests (rate limited)
// 500 — Internal Server Error
Authentication errors

When a JWT expires or is invalid, the API returns 401. Mobile clients should prompt re-authentication via Apple Sign In when receiving this status. Sessions are valid for 7 days.

Ready to integrate?

Start building with the BrewMark API. Check the embed widget docs for the easiest integration path, or use the REST API directly.