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.
| Route | Method | Auth | Status | Contract |
|---|---|---|---|---|
| /v1/catalog/{id}/similar | GET | Member/admin session or API key with API Origin or Enterprise and catalog:read | Beta | Returns target plus beta matches, score dimensions, category/confidence labels, price_delta_1lb, pricing fallbacks, and cautious copy. |
Path and query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | positive integer | required | Catalog coffee ID. The route validates against the Postgres int4 ceiling of 2147483647 before any database work. |
| threshold | number between 0.5 and 0.99 | 0.7 | Minimum similarity score sent to the matching RPC. |
| limit | positive integer up to 25 | 10 | Number of matches returned after any mode filtering. |
| stocked_only | true | false | true | Whether candidate rows must currently be stocked. |
| mode | all | likely_same | similar_profile | all | Filters 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.
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"{
"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
{
"error": "Authentication required",
"message": "Similar coffee matching requires a member account or paid API tier.",
"code": "auth_required",
"requiredCapability": "canUseBeanMatching"
}{
"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
}
}{
"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"
}
}