Pair
v1 · 2026 edition

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.

§ I · Quickstart

Sixty seconds to first call

  1. Request early access — the API is in private beta. We'll issue you a pk_* key scoped to your tier.
  2. Export the key: export PAIR_API_KEY=pk_...
  3. 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"}'
§ II · Auth & limits

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.

TierPriceCallsRateNotes
DevFree1,000 / month30 req/minattribution required
Startup$499 / mo100,000 / month120 req/minemail support
Scale$1,999 / mo1,000,000 / month600 req/minpriority email + Slack
EnterpriseCustomCustomCustomSLA · private deploy · invoice
§ III · Endpoints

Endpoint reference

POST/v1/score

Score 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.

Request body
{
  "a": "tomato",
  "b": "basil"
}
Response (200)
{
  "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
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"}'
POST/v1/swap

Find 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.

Request body
{
  "ingredient": "shallot",
  "context_basket": ["chicken", "lemon", "garlic", "shallot"],
  "exclude": ["onion"],
  "k": 5
}
Response (200)
{
  "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
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}'
POST/v1/complete

Complete 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.

Request body
{
  "basket": ["chicken", "lemon", "garlic"],
  "k": 8
}
Response (200)
{
  "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
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}'
POST/v1/embed

Embed 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.

Request body
{
  "basket": ["tomato", "basil", "olive_oil"],
  "siblings": ["core"]
}
Response (200)
{
  "basket": ["tomato", "basil", "olive_oil"],
  "unknown": [],
  "vectors": {
    "core": [0.0123, -0.0487, ..., 0.0291]
  },
  "dim": 300
}
curl
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"]}'
§ IV · Errors

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.

StatusMeaning
400Invalid request body — see issues
401Missing or invalid X-API-Key
429Rate limit exceeded — see Retry-After
500Internal error — please report
§ V · Versioning

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.