The pair.directory API.
Four endpoints — score, swap, complete, embed — backed by the three Epicure embeddings. JSON over HTTPS, authenticated with a single header. Designed to plug into recipe apps, meal-kit services, grocery delivery, and CPG product development tools.
Sixty seconds to first call
- Request early access — the API is in private beta. We'll issue you a
pk_*key scoped to your tier. - Export the key:
export PAIR_API_KEY=pk_... - Run any of the curl examples below. You should get a 200 in under 50ms.
curl -X POST https://pair.directory/api/v1/score \
-H "X-API-Key: $PAIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"a":"tomato","b":"basil"}'Authentication and rate limits
Every request must include an X-API-Key header. Missing or invalid keys return 401. Rate-limited requests return 429 with Retry-After, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
| Tier | Price | Calls | Rate | Notes |
|---|---|---|---|---|
| Dev | Free | 1,000 / month | 30 req/min | attribution required |
| Startup | $499 / mo | 100,000 / month | 120 req/min | email support |
| Scale | $1,999 / mo | 1,000,000 / month | 600 req/min | priority email + Slack |
| Enterprise | Custom | Custom | Custom | SLA · private deploy · invoice |
Endpoint reference
/v1/scoreScore a pair
Returns Cooc, Chem, and Core cosines for two ingredients, classifies the pair into one of four quadrants, and generates a plain-English verdict.
{
"a": "tomato",
"b": "basil"
}{
"a": "basil",
"b": "tomato",
"cosines": { "cooc": 0.4432, "chem": 0.4012, "core": 0.6701 },
"quadrant": "classic",
"shared_modes": [
{
"sibling": "cooc",
"slug": "cooc--italian-aromatics",
"label": "Italian aromatics",
"kind": "cuisine"
}
],
"verdict": "Basil and tomato pair classically — they're cooked together often and share aroma chemistry, so the combination is both familiar and chemically coherent. Both sit inside the \"Italian aromatics\" mode."
}curl -X POST https://pair.directory/api/v1/score \
-H "X-API-Key: $PAIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"a":"tomato","b":"basil"}'/v1/swapFind substitutes
Ranks substitutes for a single ingredient by aroma chemistry. Pass context_basket to re-rank by how much the dish centroid shifts — small delta means the dish still tastes like itself after the swap.
{
"ingredient": "shallot",
"context_basket": ["chicken", "lemon", "garlic", "shallot"],
"exclude": ["onion"],
"k": 5
}{
"ingredient": "shallot",
"context_basket": ["chicken", "lemon", "garlic", "shallot"],
"substitutes": [
{
"name": "chive",
"chem": 0.4344,
"cooc": 0.2810,
"core": 0.5612,
"quadrant": "substitute",
"top_mode": { "label": "Allium aromatics", "kind": "family" },
"centroid_delta": 0.0421,
"rationale": "chive: shares moderate aroma chemistry (chem 0.43); keeps the dish nearly unchanged (Δ 0.042); sits in the \"Allium aromatics\" flavor mode."
}
]
}curl -X POST https://pair.directory/api/v1/swap \
-H "X-API-Key: $PAIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ingredient":"shallot","context_basket":["chicken","lemon","garlic","shallot"],"k":5}'/v1/completeComplete a basket
Given a partial ingredient list, returns the highest-scoring additions across all three siblings in one call. Use it to suggest the next ingredient for a recipe or pantry.
{
"basket": ["chicken", "lemon", "garlic"],
"k": 8
}{
"basket": ["chicken", "lemon", "garlic"],
"unknown": [],
"neighbours": {
"cooc": [{ "name": "black_pepper", "cosine": 0.4817 }, ...],
"core": [{ "name": "olive_oil", "cosine": 0.7293 }, ...],
"chem": [{ "name": "shallot", "cosine": 0.4549 }, ...]
},
"modes": {
"cooc": [{ "mode_id": "...", "label": "Mediterranean roast", "kind": "cuisine", "cosine": 0.71 }],
"core": [...],
"chem": [...]
}
}curl -X POST https://pair.directory/api/v1/complete \
-H "X-API-Key: $PAIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"basket":["chicken","lemon","garlic"],"k":8}'/v1/embedEmbed a basket
Returns the L2-normalised centroid vector of an ingredient basket in each requested sibling space. Use this to power your own recipe-similarity search or vector index.
{
"basket": ["tomato", "basil", "olive_oil"],
"siblings": ["core"]
}{
"basket": ["tomato", "basil", "olive_oil"],
"unknown": [],
"vectors": {
"core": [0.0123, -0.0487, ..., 0.0291]
},
"dim": 300
}curl -X POST https://pair.directory/api/v1/embed \
-H "X-API-Key: $PAIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"basket":["tomato","basil","olive_oil"],"siblings":["core"]}'Error envelope
All errors return a JSON object with an error string and, for validation failures, an issues object from zod. Unknown ingredients are reported in an unknown array rather than failing the request.
| Status | Meaning |
|---|---|
| 400 | Invalid request body — see issues |
| 401 | Missing or invalid X-API-Key |
| 429 | Rate limit exceeded — see Retry-After |
| 500 | Internal error — please report |
The /v1 prefix is a stability promise. Breaking changes ship under a new prefix; /v1 stays callable for at least 12 months after a successor is published. Additive changes — new optional request fields, new response fields, new endpoints — can land in /v1 at any time.