Public endpoint
Catalog API
GET /v1/catalog is the stable public contract for normalized green coffee listings and tier-aware API access.
GET /v1/catalog is the canonical external endpoint. It returns normalized coffee listings with origin, legacy processing labels, structured process transparency fields, Purveyor Score metadata, pricing, price tiers, and availability metadata.
The endpoint supports three canonical auth contexts: anonymous, first-party session, and API key. Anonymous, viewer-session, and API Green requests share the basic public catalog query surface. Member/admin sessions and paid API tiers additionally unlock structured process facet filters. Public callers can still inspect factual process fields in full rows; the gated feature is process search leverage, not data visibility. API-key requests use plan-based limits and are the intended production integration path because they emit X-RateLimit-* headers and durable quota metadata. When page and limit are both omitted, the canonical listing path defaults to page 1 and up to 100 rows before any plan-based cap is applied. Explicit limit values above 1000 are rejected with HTTP 400 so pagination metadata stays truthful.
Use include=proof when callers need compact proof-summary families for process, provenance, freshness, and pricing. Proof summaries are cautious catalog signals, not certifications, and raw supplier evidence remains withheld. Use GET /v1/catalog/proof-coverage when callers need aggregate proof label distributions and gap counts for the same visible catalog scope.
Namespace and compatibility
- GET /v1 returns the public namespace descriptor and links callers to /v1/catalog and /v1/price-index.
- GET /v1/catalog is the source-of-truth public contract for integrations.
- GET /v1/catalog/proof-coverage returns aggregate proof-summary coverage for the visible catalog scope without raw evidence, supplier quotes, certification language, or row-level proof search leverage.
- GET /v1/catalog/{id}/similar is the beta matching endpoint in the catalog family. It is not anonymous, and it should be presented as candidate discovery rather than accepted identity resolution.
- GET /api/catalog-api is a deprecated API-key-only alias to the canonical handler. Responses include Deprecation: true, Link: </v1/catalog>; rel="successor-version", and Sunset: Thu, 31 Dec 2026 23:59:59 GMT.
- GET /api/catalog also delegates to the same catalog resource, but it is an internal adapter with legacy response-shape behavior and should not be treated as a long-term external contract.
External integrations should target /v1/catalog
If a caller currently uses /api/catalog-api or /api/catalog, migrate it to /v1/catalog. The v1 route is the compatibility promise. The others are transitional or first-party adapters.
Request and response
The canonical response includes data, pagination, and meta blocks. The meta block reports auth kind, role, plan, access scope, row-limit state, and cache metadata.
Full catalog rows include legacy structured processing fields, Purveyor Score fields, plus a nested process object. Null values stay null when the supplier has not disclosed structured metadata. process.evidence_available reports whether internal provenance exists without exposing raw evidence quotes in the public response.
The example below shows an API-key response. Anonymous and session responses keep the same top-level shape. The main differences are headers and search leverage: only API-key requests emit X-RateLimit-* headers, and only member/admin sessions or paid API tiers can use structured process facet filters.
Viewer-tier API keys are capped to 25 rows per call and cannot use structured process facet filters. Member and enterprise API plans remove that lower plan cap and unlock process facet filters, while still sharing the 1000-row per-request ceiling. Anonymous and viewer-session requests are public-only unless a privileged member session explicitly enables richer first-party visibility.
Cookies are not part of the public API contract. They only matter when they resolve to a valid first-party session, and the legacy /api/catalog-api alias does not accept session auth as a substitute for an API key.
{
"data": [
{
"id": 128,
"name": "Ethiopia Guji",
"region": "Guji",
"processing": "Natural",
"drying_method": "Raised beds",
"purveyor_score": 82,
"purveyor_score_tier": "Strong",
"purveyor_score_confidence": 0.78,
"purveyor_score_version": "purveyor-score-v1",
"process": {
"base_method": "Natural",
"fermentation_type": "Anaerobic",
"additives": null,
"additive_detail": null,
"fermentation_duration_hours": 72,
"drying_method": "Raised beds",
"notes": "Anaerobic natural process disclosed by supplier notes",
"disclosure_level": "high_detail",
"confidence": 0.92,
"evidence_available": true
},
"price_per_lb": 7.5,
"price_tiers": [{ "min_lbs": 1, "price": 7.5 }],
"stocked": true,
"source": "sweet_marias",
"country": "Ethiopia",
"continent": "Africa"
}
],
"pagination": {
"page": 1,
"limit": 25,
"total": 814,
"totalPages": 33,
"hasNext": true,
"hasPrev": false
},
"meta": {
"resource": "catalog",
"namespace": "/v1/catalog",
"version": "v1",
"auth": { "kind": "api-key", "role": "viewer", "apiPlan": "viewer" },
"access": {
"publicOnly": true,
"showWholesale": false,
"wholesaleOnly": false,
"rowLimit": 25,
"limited": true,
"totalAvailable": 814
},
"cache": { "hit": false, "timestamp": null }
}
}{
"data": [
{
"id": 205,
"source": "sweet_marias",
"name": "Kenya Nyeri AB",
"stocked": true,
"cost_lb": 8.1,
"price_per_lb": 8.1,
"price_tiers": [{ "min_lbs": 1, "price": 8.1 }],
"public_coffee": true
},
{
"id": 204,
"source": "cafe_imports",
"name": "Colombia Huila Washed",
"stocked": true,
"cost_lb": 7.65,
"price_per_lb": 7.65,
"price_tiers": [{ "min_lbs": 1, "price": 7.65 }],
"public_coffee": true
}
],
"pagination": {
"page": 2,
"limit": 2,
"total": 814,
"totalPages": 407,
"hasNext": true,
"hasPrev": true
},
"meta": {
"resource": "catalog",
"namespace": "/v1/catalog",
"version": "v1",
"auth": { "kind": "anonymous", "role": null, "apiPlan": null },
"access": {
"publicOnly": true,
"showWholesale": false,
"wholesaleOnly": false,
"rowLimit": null,
"limited": false,
"totalAvailable": 814
},
"cache": { "hit": false, "timestamp": null }
}
}Query parameters
The table below describes the full canonical query surface. If page is supplied without limit, the route uses a 15-row pagination fallback. If both page and limit are omitted, the canonical listing path uses the 100-row default listing contract.
Anonymous, viewer-session, and API Green requests share the basic public query surface. Structured process facet filters are gated to member/admin sessions and paid API tiers.
fields=dropdown stays compatible with normal page and limit params. The reduced projection is limited to id, source, name, stocked, cost_lb, price_per_lb, price_tiers, and public_coffee.
include=proof is opt-in. Default full rows keep their existing shape, while proof requests add a cautious proof object with process, provenance, freshness, and pricing families plus explicit limitations.
Privileged member and admin sessions may additionally use showWholesale and wholesaleOnly to widen first-party visibility. Paid API tiers unlock process facet filters but remain public-catalog scoped.
Malformed typed params now fail closed with 400 responses instead of silently falling back or bubbling into generic 500s. That applies to include, fields, stocked, showWholesale, wholesaleOnly, has_additives, sortField, sortDirection, page, limit, stocked_date, stocked_days, score_value_min, score_value_max, price_per_lb_min, price_per_lb_max, processing_confidence_min, and deprecated cost_lb aliases. Supported sortField values are arrival_date, stocked_date, name, source, continent, country, region, processing, cultivar_detail, type, grade, appearance, score_value, cost_lb, or price_per_lb.
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number for paginated results. |
| limit | integer | 100 when page and limit are both omitted; otherwise 15 fallback | Rows per page before any plan cap is applied, up to 1000. Values above 1000 return 400. |
| ids | integer (repeatable) | none | Fetch specific catalog IDs. When present, pagination is ignored and results are sorted by name ascending. |
| fields | full | dropdown | full | dropdown returns the reduced projection used by filter UIs and select menus (id, source, name, stocked, cost_lb, price_per_lb, price_tiers, public_coffee), and it works with normal page and limit params. Invalid values return 400. |
| include | proof | none | Opt-in proof summaries for full catalog rows. Unsupported include values return 400. Proof summaries include cautious family signals and limitations, not raw evidence or certification claims. |
| stocked | true | false | all | true | Filter to stocked-only, unstocked-only, or the full catalog. Invalid values return 400. |
| origin | string | none | Partial match across continent, country, and region. |
| country | string (repeatable) | none | Exact match on country. Repeat the parameter to match any of several country labels. |
| continent | string | none | Exact match on continent. |
| source | string (repeatable) | none | Repeat to filter across multiple supplier slugs. |
| processing | string | none | Partial match on the legacy processing label. This remains supported for compatibility. |
| name | string | none | Partial match on coffee name. |
| processing_base_method | string | none | Paid process facet. Exact match on normalized base process, for example Washed, Natural, Honey, Wet-Hulled, Decaf, Other, or Unknown. Requires a member/admin session or paid API tier. |
| fermentation_type | string | none | Paid process facet. Exact match on normalized fermentation technique, for example Anaerobic, Carbonic Maceration, Yeast Inoculated, Co-Fermented, None Stated, or Unknown. Requires a member/admin session or paid API tier. |
| process_additive | string | none | Paid process facet. Array containment filter for disclosed additives such as fruit, yeast, hops, mossto, starter-culture, none, or unspecified. Requires a member/admin session or paid API tier. |
| has_additives | true | false | none | Paid process facet. true returns rows with a disclosed additive value. false returns explicit none only, not unknown or unspecified rows. Requires a member/admin session or paid API tier. |
| processing_disclosure_level | string | none | Paid process facet. Exact match on supplier disclosure quality: none, label_only, structured, narrative, or high_detail. Requires a member/admin session or paid API tier. |
| processing_confidence_min | number | none | Paid process facet. Minimum 0 to 1 confidence score for the structured process breakdown. Requires a member/admin session or paid API tier. |
| region | string | none | Partial match on region. |
| cultivar_detail | string | none | Partial match on cultivar or variety detail. |
| type | string | none | Partial match on type. |
| grade | string | none | Partial match on grade. |
| appearance | string | none | Partial match on appearance. |
| price_per_lb_min / price_per_lb_max | number | none | Canonical price filters. |
| cost_lb_min / cost_lb_max | number | none | Deprecated compatibility aliases for the canonical price filters. Prefer price_per_lb_min / price_per_lb_max in new integrations. |
| score_value_min | number | none | Minimum cupping or quality score (inclusive). |
| score_value_max | number | none | Maximum cupping or quality score (inclusive). |
| arrival_date | string | none | Exact match on the stored arrival_date value. Use YYYY-MM-DD when the supplier row has a normalized date. |
| stocked_date | string (YYYY-MM-DD) | none | Filter to coffees stocked on or after a given absolute date. Invalid formats return 400. |
| stocked_days | integer | none | Filter to coffees stocked within the last N days. Use stocked_date for absolute dates. |
| showWholesale | boolean | false | Only effective for privileged member sessions. Ignored for anonymous and API-key requests. Invalid values return 400. |
| wholesaleOnly | boolean | false | Requires showWholesale=true and a privileged member session. Invalid values return 400. |
| sortField | arrival_date, stocked_date, name, source, continent, country, region, processing, cultivar_detail, type, grade, appearance, score_value, cost_lb, or price_per_lb | arrival_date | Sort field for non-ID queries. Invalid values return 400. |
| sortDirection | asc | desc | desc | Sort direction for non-ID queries. |
curl "https://purveyors.io/v1/catalog?fields=dropdown&page=2&limit=15"Proof coverage aggregate
GET /v1/catalog/proof-coverage summarizes the same proof-summary vocabulary exposed by include=proof. It reports overall labels, family label distributions, signal counts, top missing families, and explicit limitations for the visible catalog scope.
The endpoint is aggregate-only. It is safe as a public proof-of-value surface because it does not expose raw processing_evidence, raw supplier quotes, row-level evidence, certification claims, supplier rankings, or paid proof-query filters.
API-key requests preserve X-RateLimit-* headers and plan-scoped visibility. Anonymous and session requests follow the same catalog visibility and process-facet capability rules as /v1/catalog.
{
"resource": "catalog-proof-coverage",
"namespace": "/v1/catalog/proof-coverage",
"version": "v1",
"scope": { "total_rows": 814 },
"overall": [{ "label": "strong", "count": 488, "share": 0.6 }],
"families": {
"process": [{ "label": "disclosed", "count": 260, "share": 0.319 }]
},
"signals": { "process.base_method": 260 },
"top_gaps": [{ "family": "process", "label": "not_available", "count": 320, "share": 0.393 }],
"limitations": ["not_certification", "raw_evidence_not_included"]
}curl "https://purveyors.io/v1/catalog/proof-coverage?stocked=true" \ -H "Authorization: Bearer $PURVEYORS_API_KEY"Structured process filter edge cases
- processing remains the backward-compatible public partial match against the legacy display label. Use the structured filters when an integration has member/admin session access or a paid API tier and needs process transparency semantics instead of text search.
- processing_base_method, fermentation_type, process_additive, processing_disclosure_level, and processing_confidence_min only match rows where the structured metadata is present. Null supplier metadata is preserved and should not be treated as explicit none. These params return 401 for anonymous callers and 403 for viewer/API Green callers.
- has_additives=true matches rows with disclosed additive values such as fruit, yeast, hops, mossto, or starter-culture. has_additives=false matches only rows whose additive array is exactly none; it intentionally excludes unknown, unspecified, null, or mixed values. This is also gated as a process facet.
- process_additive is an array-containment filter. A row with multiple disclosed additives can match any one repeated request pattern only by issuing separate requests today.
- Full rows include process.evidence_available but never expose raw processing_evidence quotes. The dropdown projection does not include the nested process object.
- include=proof adds proof.families.process, provenance, freshness, and pricing plus limitations such as not_certification and raw_evidence_not_included. Badge copy in public cards uses the same cautious vocabulary: disclosed, identified, dated, listed, or tiered.
curl "https://purveyors.io/v1/catalog?fermentation_type=Co-Fermented&has_additives=true&limit=25" \
-H "Authorization: Bearer pk_live_origin_or_enterprise_key"
curl "https://purveyors.io/v1/catalog?has_additives=false&processing_disclosure_level=structured&limit=25" \
-H "Authorization: Bearer pk_live_origin_or_enterprise_key"Proof summaries
include=proof adds a compact proof object to full catalog rows. It is designed for public cards, agent summaries, and integration UIs that need to explain why a listing looks trustworthy without exposing raw supplier evidence.
The proof object groups signals into process, provenance, freshness, and pricing families. Each family uses cautious labels and limitations. It is not a certification system, and it does not expose raw processing evidence quotes by default.
curl "https://purveyors.io/v1/catalog?include=proof&country=Ethiopia&limit=5" \
-H "Authorization: Bearer pk_live_your_key_here"{
"proof": {
"families": {
"process": { "label": "disclosed", "confidence": 0.92 },
"provenance": { "label": "identified" },
"freshness": { "label": "dated" },
"pricing": { "label": "tiered" }
},
"limitations": ["not_certification", "raw_evidence_not_included"]
}
}Access mode comparison
- Anonymous, viewer-session, and API Green calls share the basic public query surface. Process facet params return 401 for anonymous callers and 403 for viewer/API Green callers.
- If an Authorization header is present but invalid, the route returns 401 instead of silently treating the request as anonymous.
| Mode | Best for | Query envelope | Headers | Notes |
|---|---|---|---|---|
| Anonymous /v1/catalog | Discovery, evaluation, and public embeds | Basic public query surface only. Structured process facets return 401. Defaults to 100 rows when page and limit are omitted; page without limit falls back to 15. | Content-Type only | Public-only catalog data. No X-RateLimit-* headers. |
| API-key /v1/catalog | Production integrations and accounted usage | Basic public query surface for API Green; paid API tiers add structured process facets. Defaults to 100 rows when page and limit are omitted. | Content-Type plus X-RateLimit-* | Canonical integration path for developers, sync jobs, and agents. API Green is for evaluation; API Origin and Enterprise unlock process search leverage. |
| Session /v1/catalog | First-party product reads | Viewer sessions stay public-only. Member/admin sessions unlock process facets and may also unlock showWholesale and wholesaleOnly. | Session-dependent app headers only | Cookies are only relevant when they resolve to a valid first-party session. |
| GET /api/catalog-api | Legacy API-key callers during migration | Uses the same API-key query contract as /v1/catalog. | Deprecation, Sunset, Link, plus X-RateLimit-* | API-key-only deprecated alias. Sunset: Dec 31 2026. |
Example requests
curl "https://purveyors.io/v1/catalog?country=Ethiopia&processing=Natural&limit=15"curl "https://purveyors.io/v1/catalog?stocked_days=30&price_per_lb_max=9&limit=50" -H "Authorization: Bearer pk_live_your_key_here"const response = await fetch("https://purveyors.io/v1/catalog?country=Colombia&limit=25", {
headers: { Authorization: `Bearer ${process.env.PARCHMENT_API_KEY}` }
});
const payload = await response.json();
console.log(payload.meta.access, payload.data.length);import os
import requests
response = requests.get(
"https://purveyors.io/v1/catalog",
params={"processing": "washed", "limit": 25},
headers={"Authorization": f"Bearer {os.environ['PARCHMENT_API_KEY']}"},
timeout=30,
)
response.raise_for_status()
payload = response.json()
print(payload["pagination"]["total"], payload["meta"]["auth"])Tier limits and headers
- The public docs use marketed tier names Green, Origin, and Enterprise, while API responses and server code use apiPlan keys viewer, member, and enterprise.
- API Green can read factual process fields in full catalog rows, but structured process facet filtering starts at API Origin.
- All callers share a hard per-request page-size ceiling of 1000, even when a paid plan removes the lower viewer-tier row cap.
- X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset are only emitted for API-key responses.
- 429 responses also include Retry-After.
- Anonymous and session-based catalog requests are not counted against an API-key quota and therefore do not receive those headers.
| Marketed plan | Code key | Monthly requests | Rows per call | Notes |
|---|---|---|---|---|
| Green | viewer | 200 | 25 | Best for evaluation and prototypes. Includes public response fields but not structured process facet filtering. |
| Origin | member | 10,000 | Up to 1000 per request | No additional plan row cap beyond the shared request-size ceiling. Best for production integrations and sync jobs. |
| Enterprise | enterprise | Unlimited | Up to 1000 per request | No additional plan row cap beyond the shared request-size ceiling. Contact sales for commercial volume and support. |
Related links
Plans, positioning, and quick start.
See the product surface that consumes the same data model.
Find candidate matches for a catalog coffee with member or paid API access.
Internal /api/* companions to the public contract.
Terminal access to the same catalog domain.