Beta endpoint

Catalog similarity API

GET /v1/catalog/{id}/similar returns beta similar-coffee candidates for member sessions and paid API-key integrations.

GET /v1/catalog/{id}/similar finds candidate coffees related to one catalog entry. The route is useful for likely-same-bean checks, substitution research, account-linked agents, and pricing context around comparable lots.

Matches are beta confidence candidates based on origin, processing, and tasting similarity signals. The endpoint intentionally does not claim canonical identity. UI copy and API responses should keep that cautious framing.

Endpoint and access

  • Anonymous callers receive 401 auth_required. The response does not leak match data.
  • Signed-in viewer sessions and API Green keys receive 403 entitlement_required. Viewer sessions can receive a locked teaser count when the target exists, but not match rows.
  • API-key callers must satisfy requiredPlan member and requiredScope catalog:read. Successful API-key responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.
  • 429 responses use the same quota envelope as /v1/catalog and include Retry-After.
  • 404 means the target catalog coffee was not found after the caller has enough access to request matches.
RouteMethodAuthStatusContract
/v1/catalog/{id}/similarGETMember/admin session or API key with API Origin or Enterprise and catalog:readBetaReturns target plus beta matches, score dimensions, category/confidence labels, price_delta_1lb, pricing fallbacks, and cautious copy.

Path and query parameters

ParameterTypeDefaultDescription
idpositive integerrequiredCatalog coffee ID. The route validates against the Postgres int4 ceiling of 2147483647 before any database work.
thresholdnumber between 0.5 and 0.990.7Minimum similarity score sent to the matching RPC.
limitpositive integer up to 2510Number of matches returned after any mode filtering.
stocked_onlytrue | falsetrueWhether candidate rows must currently be stocked.
modeall | likely_same | similar_profileallFilters normalized matches after scoring. likely_same asks the server to overfetch before filtering so likely-same rows are not hidden by profile matches.

Structured validation is part of the contract

Invalid id, threshold, limit, stocked_only, or mode values return HTTP 400 with error: Invalid query parameter and a details block containing parameter, value, and expected.

Response shape

data.target summarizes the requested coffee with origin, process, stocked state, legacy cost_lb compatibility, and canonical pricing fields.

data.matches contains candidate coffees. Each match includes coffee identity fields, canonical pricing, price_delta_1lb, score.average, score.dimensions.origin, score.dimensions.processing, score.dimensions.tasting, score.chunk_matches, match.category, match.confidence, match.beta, match.language, explanation.summary, explanation.signals, and compatibility.cost_lb.

meta.status is beta and meta.copy.confidence repeats the non-canonical identity warning. Preserve that framing in client copy.

Member or API-key request
curl "https://purveyors.io/v1/catalog/1182/similar?threshold=0.8&limit=5&mode=likely_same" \
  -H "Authorization: Bearer pk_live_origin_or_enterprise_key"
Successful response fragment
{
  "data": {
    "target": {
      "id": 1182,
      "name": "Ethiopia Guji Natural",
      "source": "Supplier A",
      "origin": "Guji",
      "country": "Ethiopia",
      "processing": "Natural",
      "stocked": true,
      "pricing": {
        "price_per_lb": null,
        "price_tiers": [{ "min_lbs": 1, "price": 8 }],
        "cost_lb": 7.1,
        "baseline_quantity_lbs": 1,
        "baseline_price_per_lb": 8,
        "baseline_source": "price_tiers"
      }
    },
    "matches": [
      {
        "coffee": { "id": 2200, "name": "Ethiopia Guji Natural Lot B", "stocked": true },
        "pricing": { "baseline_price_per_lb": 8.75, "baseline_source": "price_per_lb" },
        "price_delta_1lb": { "amount": 0.75, "percent": 9.4, "currency": "USD" },
        "score": {
          "average": 0.92,
          "dimensions": { "origin": 0.94, "processing": 0.91, "tasting": 0.87 },
          "chunk_matches": 3
        },
        "match": {
          "category": "likely_same",
          "confidence": "high_beta",
          "beta": true,
          "language": "High beta confidence likely same coffee candidate. Review supplier details before acting."
        },
        "explanation": {
          "summary": "Beta similarity score based on available origin, processing, and tasting embeddings.",
          "signals": ["Origin similarity 0.94", "Processing similarity 0.91"]
        }
      }
    ]
  },
  "meta": {
    "resource": "catalog-similarity",
    "namespace": "/v1/catalog/{id}/similar",
    "version": "v1",
    "status": "beta",
    "auth": { "kind": "api-key", "role": "viewer", "apiPlan": "member" },
    "access": { "requiredCapability": "canUseBeanMatching", "canUseBeanMatching": true },
    "query": { "threshold": 0.8, "limit": 5, "stockedOnly": true, "mode": "likely_same" }
  }
}

Error examples

401 anonymous request
{
  "error": "Authentication required",
  "message": "Similar coffee matching requires a member account or paid API tier.",
  "code": "auth_required",
  "requiredCapability": "canUseBeanMatching"
}
403 locked viewer teaser
{
  "error": "Insufficient permissions",
  "message": "Similar coffee matching is available to members and paid API tiers.",
  "code": "entitlement_required",
  "requiredCapability": "canUseBeanMatching",
  "teaser": {
    "locked": true,
    "similar_match_count": 4,
    "beta": true
  }
}
400 invalid query parameter
{
  "error": "Invalid query parameter",
  "message": "Query parameter limit must use positive integer less than or equal to 25",
  "details": {
    "parameter": "limit",
    "value": "99",
    "expected": "positive integer less than or equal to 25"
  }
}

Related links