From zero to a working wine pairing in five minutes. Send a sentence like “grilled salmon with lemon and dill” and get back ranked wine styles with a 0–100 match score. No setup, no SDK — just curl.
Sign up on the developer portal. You get 50 free calls per day, no credit card required.
Go to business.sommelierx.com, create an account, and copy your key. It starts with sk_live_ followed by 32 hex characters.
Export it as an environment variable so you can use it across the rest of this guide:
export SOMMELIERX_API_KEY="sk_live_..."
Most pairing tasks fit into one of six patterns. Find your input on the left, use the endpoint on the right.
| I have… | Use this endpoint | Tier |
|---|---|---|
| A sentence: “grilled salmon with dill” | POST /pairing/by-text |
FREE |
A list of ingredient names: ["salmon", "dill"] |
POST /pairing/by-text(use the ingredients array, skips the AI step) |
FREE |
| A meal ID from our catalog | POST /pairing/by-meal/{mealId} |
FREE |
| Ingredient IDs with amount and preparation | POST /pairing/calculate |
FREE |
| A wine type ID (find dishes for it) | GET /pairing/by-wine/{wineId}/meals |
FREE |
| A specific shortlist of wines to score | POST /pairing/by-ingredients-wines |
PRO |
| Multiple courses, one wine to share | POST /pairing/group |
PRO |
| An occasion tag (BBQ, romantic dinner, …) | POST /pairing/by-occasion |
PRO |
GET /search?q=… to look them up, or just send the dish name to /pairing/by-text — the API resolves ingredient names against the database for you.
Each block below is a complete request with a real, unedited response from api.sommelierx.com. Copy, paste, run.
The simplest entry point: send a sentence in any language. The API extracts ingredients, resolves them to database IDs, and returns ranked wine styles.
curl -X POST https://api.sommelierx.com/api/v1/pairing/by-text \
-H "Authorization: Bearer $SOMMELIERX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "grilled salmon with lemon and dill",
"language": "en"
}'
Response (PRO tier, top 2 of 25 results shown):
{
"data": {
"resolved_ingredients": [
{ "input": "salmon", "matched": true, "ingredient": { "id": 6144, "name": "Salmon" } },
{ "input": "lemon", "matched": true, "ingredient": { "id": 3656, "name": "Lemon" } },
{ "input": "dill", "matched": true, "ingredient": { "id": 3792, "name": "Dill" } }
],
"pairing_results": [
{
"metaWineId": 723,
"name": "Semillon-Sauvignon New World Top",
"type": { "id": 4, "name": "White dry", "color": "white" },
"region": "New World",
"grapes": ["Sauvignon Blanc", "Sémillon"],
"score": {
"match_percentage": 88,
"basic_score": 9,
"balance_score": 0,
"aromatic_score": 9
},
"priceSegment": "top",
"whyText": "Sauvignon Blanc's herbaceous intensity amplifies dill's anise notes while Sémillon's waxy texture matches salmon's richness."
},
{
"metaWineId": 1034,
"name": "Arinto and Fernão Pires Portugal Basic",
"type": { "id": 4, "name": "White dry", "color": "white" },
"region": "Portugal",
"grapes": ["Arinto", "Fernão Pires", "Sauvignon Blanc", "Viognier"],
"score": {
"match_percentage": 88,
"basic_score": 9,
"balance_score": 0,
"aromatic_score": 9
},
"priceSegment": "basic"
}
]
},
"meta": {
"tier": "pro",
"calls_remaining_today": 4998,
"rate_limit_reset": "2026-04-17T00:00:00.000Z"
}
}
What you get back. Up to 25 wine styles ranked by match_percentage, filtered to a minimum of 65/100. Each result includes the wine’s name, color, region, grapes, a price segment hint (basic / middle / top), and on PRO tier a one-sentence whyText explaining the match.
If you already have a meal ID from GET /meals or GET /search, skip the natural-language step.
curl -X POST https://api.sommelierx.com/api/v1/pairing/by-meal/5572 \
-H "Authorization: Bearer $SOMMELIERX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "language": "en" }'
Response (meal 5572 = “Salmon sashimi”, top result shown):
{
"data": {
"mealId": 5572,
"results": [
{
"metaWineId": 790,
"name": "Chardonnay Southern Europe Middle",
"type": { "id": 4, "name": "White dry", "color": "white" },
"region": "Southern Europe",
"grapes": ["Chardonnay"],
"glassType": { "id": 4, "name": "Burgundy White" },
"score": {
"match_percentage": 87,
"basic_score": 9,
"balance_score": 0,
"aromatic_score": 9
},
"priceSegment": "middle",
"whyText": "Southern Europe's warm-climate Chardonnay's mineral backbone amplifies the salmon's natural oils while respecting its pristine texture."
}
]
},
"meta": {
"tier": "pro",
"calls_remaining_today": 4997,
"rate_limit_reset": "2026-04-17T00:00:00.000Z"
}
}
Dinner party with a chicken starter, a vegetable main, and a steak? /pairing/group finds wines that score well across every meal, not just the average.
curl -X POST https://api.sommelierx.com/api/v1/pairing/group \
-H "Authorization: Bearer $SOMMELIERX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mealIds": [4329, 4330, 4331],
"language": "en"
}'
Response (top wine shown):
{
"data": {
"meals": [
{ "id": 4329, "name": "Fried chicken with beurre blanc and leaf spinach" },
{ "id": 4330, "name": "Puff pastry tartlet with vegetables" },
{ "id": 4331, "name": "Fried Angus rib-eye with Café de Paris" }
],
"wines": [
{
"metaWineId": 1152,
"name": "Bobal Spain Basic",
"type": { "id": 10, "name": "Red dry", "color": "red" },
"region": "Spain",
"priceSegment": "basic",
"avgScore": 75,
"mealCoverage": 3,
"totalMeals": 3,
"perMealScores": [
{ "mealId": 4329, "mealName": "Fried chicken with beurre blanc and leaf spinach", "score": 74 },
{ "mealId": 4330, "mealName": "Puff pastry tartlet with vegetables", "score": 75 },
{ "mealId": 4331, "mealName": "Fried Angus rib-eye with Café de Paris", "score": 75 }
]
}
]
},
"meta": {
"tier": "pro",
"calls_remaining_today": 4992,
"rate_limit_reset": "2026-04-17T00:00:00.000Z"
}
}
Read this carefully. mealCoverage = how many of the supplied meals scored above the threshold. avgScore = average across covered meals. A wine with mealCoverage: 2 may have a higher average than one with mealCoverage: 3 — sort by whichever matters more for your use case.
Every pairing returns a score object. On FREE tier you get match_percentage only. On PRO and ENTERPRISE you also see the three components that produced it.
integer 0–100
The headline number. Anything below 65 is filtered out. 80+ is a confident pairing; 90+ is exceptional.
integer 0–10
Body, structure, and weight match. Does the wine have enough density to stand up to the dish without overwhelming it?
integer (penalty)
Tannin/acid/sweetness clash penalty. Positive values reduce the final score by 0.5x; negative values reduce it by 1x. Lower (or zero) is better.
integer 0–10 or null
Flavor-profile resonance — herbal, fruit, smoke, mineral. null means the algorithm didn’t use aromatic data for this pairing.
How they combine.
// 1. Average the basic and aromatic scores (or use basic only if aromatic is null)
final = aromatic_score !== null
? (basic_score + aromatic_score) / 2
: basic_score
// 2. Apply the balance penalty
final = balance_score > 0
? final - (0.5 * balance_score)
: final - (-1 * balance_score)
// 3. Scale to 0-100
match_percentage = Math.ceil(final * 10)
balance_score: 0 means no clash — the wine’s tannin, acidity, and sweetness all sit comfortably with the dish. That’s a good thing: the final match is the average of basic and aromatic, with no penalty.
If you don’t want to write code, drop the SommelierX MCP server into any MCP-compatible client and ask in plain English.
Add this to your client’s MCP config (e.g. Claude Desktop’s claude_desktop_config.json):
{
"mcpServers": {
"sommelierx": {
"command": "npx",
"args": ["@sommelierx/mcp-server"],
"env": {
"SOMMELIERX_API_KEY": "sk_live_..."
}
}
}
}
Restart your client and ask things like:
The SOMMELIERX_API_KEY env var is optional — without it you get the same FREE tier limits, just keyed to your IP.
| Status | Code | What it means |
|---|---|---|
| 401 | API_KEY_MISSING | No Authorization header. Pass Bearer sk_live_.... |
| 401 | API_KEY_INVALID | Key is malformed or doesn’t exist. Check for stray whitespace. |
| 403 | TIER_REQUIRED | You called a PRO endpoint on a FREE key. The response includes upgrade_url. |
| 429 | RATE_LIMIT_DAILY | Daily quota exhausted. The X-RateLimit-Reset header tells you when it resets. |
| 429 | RATE_LIMIT_MINUTE | Per-minute burst exceeded (2/min on FREE, 20/min on PRO). Add a Retry-After backoff. |
| 400 | INVALID_INPUT | Validation failed. The details array lists the offending fields. |
Free key in under a minute. No credit card. 50 calls per day to start.