Documentation

Documentation

Everything to build on Sommelier Engine.

POST /compose
curl -X POST https://api.sommelierengine.ai/compose \
  -H "Content-Type: application/json" \
  -d '{"identity":"Barolo","vintage_year":2016}'

Interactive reference and a live Playground ship with the Console. Quick Start below and most Guides now have real, contract-sourced content. Rate limits and SDKs stay honestly "planned" — neither has a real answer yet, and this page won't invent one.

Quick Start

Authentication

Not specified in API_CONTRACT_v1.md — the contract has no authentication section for any endpoint. Measured live against the deployed origin (`serve/app.py`): the service has no authentication code path at all, and a request with no `Authorization` header and a request carrying an invalid Bearer token both returned the same 200 as a normal call — this API currently performs no authentication. Shown on this page as a reconciliation flag, not as a contract fact.

docs/API_CONTRACT_v1.md — full-file read, no auth section

Your first request

The fastest way to see the API respond is POST /compose with a wine identity the registry already knows (docs/API_CONTRACT_v1.md:29-48). The call and response below are the same live-captured pair shown in Examples further down this page — repeated here, not re-derived, so Quick Start and Examples can never quietly disagree.

curl -X POST http://127.0.0.1:8081/compose \
  -H "Content-Type: application/json" \
  -d '{"identity":"Barolo"}'
{
  "snapshot_id": "sha256:08dcec31da3d2bc6c9d23abbd28d6d6a03c636bc5ed6cc9a98307e60b1fb8acc",
  "handle": "[email protected]/1.1",
  "identity": "Barolo",
  "version": { "research": "1.1", "composition": "1.1", "engine": "0.1", "api": "v1" },
  "profile": {
    "descriptors": [
      { "descriptor": "cherry", "confidence": 0.85, "dominant_mechanism": "varietal_aroma" },
      { "descriptor": "tar", "confidence": 0.85, "dominant_mechanism": "varietal_aroma" },
      { "descriptor": "truffle", "confidence": 0.85, "dominant_mechanism": "varietal_aroma" }
      /* 9 more descriptors trimmed for this page — full list in the evidence artifact */
    ],
    "axes": {
      "tannin": { "value": 0.8, "label": "high" },
      "acidity": { "value": 0.8, "label": "high" },
      "body": { "value": 0.5, "label": "medium-full" },
      "sweetness": { "value": 0.05, "label": "dry" },
      "alcohol": { "value": 0.65, "label": "medium-high" }
    },
    "confidence": { "final": 0.85 },
    "warnings": [],
    "is_blend": true,
    "vintage_available": false,
    "bottle_evolution_available": false
  }
}

docs/API_CONTRACT_v1.md:29-48

Versioning

Four independent version axes: research, composition, engine (runtime), and api (this contract) (docs/API_CONTRACT_v1.md:7-9). A client pins api=v1 and negotiates the knowledge triple; engine internals may change with no api bump as long as the response shapes hold. GET /capabilities, in API Reference below, reports the exact version quad the service is running right now.

docs/API_CONTRACT_v1.md:7-9

API Reference

Sourced from docs/API_CONTRACT_v1.md, the Knowledge Service's versioned API contract — every field below cites the contract line it comes from. Not specified in API_CONTRACT_v1.md — the contract has no authentication section for any endpoint. Measured live against the deployed origin (`serve/app.py`): the service has no authentication code path at all, and a request with no `Authorization` header and a request carrying an invalid Bearer token both returned the same 200 as a normal call — this API currently performs no authentication. Shown on this page as a reconciliation flag, not as a contract fact.

GET/capabilitiesAPI_CONTRACT_v1.md:17-27

Capability and version negotiation.

Response — Contract example (verbatim)
{
  "research": "1.1", "composition": "1.1", "engine": "0.1", "api": "v1",
  "supports": { "compose": true, "compose_bulk": true, "lookup": true, "search": true,
                "explain": true, "validate": true, "health": true, "pairing": false },
  "explain_levels": ["minimal","standard","debug"],
  "search_kinds": ["descriptor","mechanism","identity","grape","region","style","alias","canonical_id"],
  "async": { "supported": false, "ready": true }
}
Errors

The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it.

POST/composeAPI_CONTRACT_v1.md:29-48

Compose a versioned Knowledge Snapshot for a wine identity.

Request fields
identityrequiredstring (per response example `"identity": "Barolo"`, API_CONTRACT_v1.md:35)
vintage_yearoptionalnot specified in contract (name and `?` only)
bottle_ageoptionalnot specified in contract (name and `?` only)
bottle_noteoptionalnot specified in contract (name and `?` only)
explain_leveloptionalenum: minimal|standard|debug (enum sourced from GET /capabilities' explain_levels field (API_CONTRACT_v1.md:23); POST /compose's own line does not re-enumerate it)
Response — Contract example (verbatim)
{
  "snapshot_id": "sha256:…",
  "handle": "[email protected]/1.1",
  "identity": "Barolo",
  "version": { "research":"1.1","composition":"1.1","engine":"0.1","api":"v1" },
  "profile": {
    "descriptors": [ { "descriptor":"tar","confidence":0.9,"dominant_mechanism":"varietal_aroma" } ],
    "axes": { "acidity": {"value":0.8}, ... },
    "confidence": { "final":0.85 },
    "warnings": [],
    "is_blend": false,
    "vintage_available": false,
    "bottle_evolution_available": false
  }
}

Contract's own example truncates `axes` with `...`; not filled in here to avoid fabricating a shape the contract doesn't give. `snapshot_id = sha256(identity + version triple + args + resolved profile)` (API_CONTRACT_v1.md:48).

Errors

The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it.

Live-observed (this task)
  • Live-observed: an unknown identity returns HTTP 400 (evidence artifact quotes the transcript), not the 404 the contract's status table states (docs/API_CONTRACT_v1.md:106).
  • Live-observed: a request missing the required `identity` field returns a raw Python `TypeError` message, not the typed `BadRequest` code the contract promises (docs/API_CONTRACT_v1.md:91, :105).
POST/compose/bulkAPI_CONTRACT_v1.md:50-62

Compose many wines in one request (async-ready envelope).

Request fields
itemsrequiredarray of { identity, vintage_year?, bottle_age? } — up to N (contract states "up to N" without giving N)
items[].identityrequiredstring
items[].vintage_yearoptionalnot specified in contract
items[].bottle_ageoptionalnot specified in contract
explain_leveloptionalenum: minimal|standard|debug (enum sourced from GET /capabilities' explain_levels field (API_CONTRACT_v1.md:23); POST /compose's own line does not re-enumerate it)
Response — Contract example (verbatim)
{
  "mode": "sync",
  "job": null,
  "count": 200,
  "results": [ { "ok": true, "snapshot_id":"…", "identity":"…", "profile":{…} },
               { "ok": false, "identity":"Xyz", "error":"UnknownIdentity" } ]
}

Async-ready: when async lands, `mode:"async"` + `job:{id,status,poll_url}` and `results:null` until ready — same envelope, no client break (API_CONTRACT_v1.md:61-62).

Errors

The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it. Note the response envelope's own per-item `error` field (e.g. "UnknownIdentity") reuses the global taxonomy's codes (API_CONTRACT_v1.md:58).

GET/lookupAPI_CONTRACT_v1.md:64

Fetch a Knowledge Snapshot by identity (no vintage/bottle args).

Request fields
identityrequiredstring (contract: "(no vintage/bottle args)" implies identity is the only param)
Response — Same response shape as POST /compose (contract: "same ComposeResponse")
{
  "snapshot_id": "sha256:…", "handle": "[email protected]/1.1", "identity": "Barolo",
  "version": { "research":"1.1","composition":"1.1","engine":"0.1","api":"v1" },
  "profile": { "descriptors":[...], "axes":{...}, "confidence":{"final":0.85}, "warnings":[],
               "is_blend": false, "vintage_available": false, "bottle_evolution_available": false }
}

"(no vintage/bottle args)" (API_CONTRACT_v1.md:64) scopes the REQUEST, not the response — the contract states the response is identical to POST /compose's.

Errors

The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it.

POST/explainAPI_CONTRACT_v1.md:74-78

Causal mechanism graph for a descriptor, at a chosen detail level.

Request fields
identityrequiredstring
descriptorrequiredstring (e.g. "tar" per API_CONTRACT_v1.md:38, shown there as a POST /compose descriptor value)
leveloptionalenum: minimal|standard|debug
Response — Illustrative — not in contract
{ "descriptor": "tar", "dominant_mechanism": "varietal_aroma", "confidence": 0.9 }

The contract gives no JSON example for /explain, only prose per level (API_CONTRACT_v1.md:76-78): minimal = {descriptor, dominant_mechanism, confidence}; standard = minimal + supporting_mechanisms and an evidence summary; debug = full mechanism graph (paths, suppressed duplicates, disagreement). This illustrative example covers only the minimal shape using field names the contract states; standard/debug are not JSON-ized here since the contract gives no field-level shape for either.

Errors

The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it.

POST/validateAPI_CONTRACT_v1.md:80-82

Validate an object against a kind. Creates nothing.

Request fields
objrequirednot specified in contract (opaque object; shape depends on `kind`)
kindrequiredenum: version|snapshot|import|identity
Response — Illustrative — not in contract
{ "ok": true, "errors": [], "version": { "research":"1.1","composition":"1.1","engine":"0.1","api":"v1" } }

Contract states the shape as prose (`{ok, errors[], version}`, API_CONTRACT_v1.md:81) but gives no JSON example and does not state what strings populate `errors[]` on a failed validation.

Errors

This endpoint's own `errors[]` response field (API_CONTRACT_v1.md:81) is a generic array whose contents the contract does not specify — it is not stated to reuse the global typed-error taxonomy below. The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it.

Live-observed (this task)
  • Live-observed: the `version` object in a /validate response uses different key names (`research_version` / `composition_version` / `runtime_version`) than every other endpoint's `version` object (`research` / `composition` / `engine` / `api`) — the contract does not document this endpoint's `version` shape separately.
GET/healthAPI_CONTRACT_v1.md:84-86

Readiness and version report.

Response — Illustrative — not in contract
{ "status": "ready",
  "version": { "research":"1.1","composition":"1.1","engine":"0.1","api":"v1" },
  "registry": { "loaded": true, "identities": 125, "registries": 1, "caps": {} },
  "cache": {}, "tests": { "composition": "pass", "cross_layer_audit": "pass", "integration": "pass", "golden": "pass" } }

Contract states the field list as prose (API_CONTRACT_v1.md:85-86), not a JSON example. Field names are transcribed as given; nested value shapes (`registry.identities` count, `cache`/`tests` contents) are illustrative placeholders, not contract-stated values.

Errors

Explicitly stated in contract, unlike other endpoints: returns HTTP 200 when ready, 503 when not ready; readiness = knowledge+registries+golden+active bundle (API_CONTRACT_v1.md:110). The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it.

Live-observed (this task)
  • Live-observed: the response wraps the contract's flat `{status, version, registry, cache, tests}` fields (docs/API_CONTRACT_v1.md:85-86) inside `{ready, health, active_version}` instead of returning them at the top level. Full transcript (including current test-suite counters, not reproduced on this public page) is in the evidence artifact for this task.
Global typed-error taxonomy

The contract does not scope error codes per endpoint (API_CONTRACT_v1.md:90-92, :100-110 define one global typed-error taxonomy and HTTP-status mapping, not a per-endpoint list). Shown below is that global taxonomy; which subset a given endpoint can actually raise is not stated in the contract, so none is asserted here beyond it.

BadRequest400API_CONTRACT_v1.md:105
UnknownIdentity404API_CONTRACT_v1.md:106
UnknownGrape404API_CONTRACT_v1.md:106
UnknownRegion404API_CONTRACT_v1.md:106
UnknownDescriptorMechanism404API_CONTRACT_v1.md:106
VersionIncompatible409API_CONTRACT_v1.md:107
CorruptedProvenance503API_CONTRACT_v1.md:108
PartialResearchVersion503API_CONTRACT_v1.md:108

Examples

Real request/response pairs captured against the running Knowledge Service for this page (2026-08-23T03:44:00Z–2026-08-23T03:46:00Z) — not hand-written illustrations. One is an honest abstention: the engine refuses to guess a profile for a wine it doesn't know.

POST /compose — a known identityHTTP 200
curl -X POST http://127.0.0.1:8081/compose \
  -H "Content-Type: application/json" \
  -d '{"identity":"Barolo"}'
{
  "snapshot_id": "sha256:08dcec31da3d2bc6c9d23abbd28d6d6a03c636bc5ed6cc9a98307e60b1fb8acc",
  "handle": "[email protected]/1.1",
  "identity": "Barolo",
  "version": { "research": "1.1", "composition": "1.1", "engine": "0.1", "api": "v1" },
  "profile": {
    "descriptors": [
      { "descriptor": "cherry", "confidence": 0.85, "dominant_mechanism": "varietal_aroma" },
      { "descriptor": "tar", "confidence": 0.85, "dominant_mechanism": "varietal_aroma" },
      { "descriptor": "truffle", "confidence": 0.85, "dominant_mechanism": "varietal_aroma" }
      /* 9 more descriptors trimmed for this page — full list in the evidence artifact */
    ],
    "axes": {
      "tannin": { "value": 0.8, "label": "high" },
      "acidity": { "value": 0.8, "label": "high" },
      "body": { "value": 0.5, "label": "medium-full" },
      "sweetness": { "value": 0.05, "label": "dry" },
      "alcohol": { "value": 0.65, "label": "medium-high" }
    },
    "confidence": { "final": 0.85 },
    "warnings": [],
    "is_blend": true,
    "vintage_available": false,
    "bottle_evolution_available": false
  }
}

Captured live, this task. Trimmed to 3 of 12 descriptors for page length — the untrimmed response is quoted verbatim in the evidence artifact. Field names and shape are unedited.

POST /explain — why a descriptor is presentHTTP 200
curl -X POST http://127.0.0.1:8081/explain \
  -H "Content-Type: application/json" \
  -d '{"identity":"Barolo","descriptor":"tar"}'
{
  "descriptor": "tar",
  "dominant_mechanism": "varietal_aroma",
  "supporting_mechanisms": [],
  "confidence": 0.85,
  "evidence": [["grape baseline"]]
}

Captured live, this task, with no `level` field sent. The default response already includes `supporting_mechanisms` and `evidence` — fields the contract's prose (docs/API_CONTRACT_v1.md:77) assigns to the `standard` level, not `minimal`. The contract states no default level, so which level an omitted field resolves to is not specified there; this is what was observed live, not a contract claim.

POST /compose — honest abstention on an unknown identityHTTP 400 — abstention
curl -X POST http://127.0.0.1:8081/compose \
  -H "Content-Type: application/json" \
  -d '{"identity":"NotARealWineXYZ123"}'
{
  "error": "UnknownIdentity",
  "message": "NotARealWineXYZ123"
}

Captured live, this task. No profile is guessed for an identity outside the registry — the engine returns the typed error named in the contract's error list (docs/API_CONTRACT_v1.md:91) instead of a best-effort fabrication. Live-observed status is 400; the contract's own HTTP-status table (docs/API_CONTRACT_v1.md:106) maps UnknownIdentity to 404 — that mismatch is real and unresolved, not corrected here.

Guides

Sourced from the same docs/API_CONTRACT_v1.md as API Reference above — these expand on contract lines already cited there, not new claims.

Error handling

Every typed error (BadRequest, UnknownIdentity, UnknownGrape, UnknownRegion, UnknownDescriptorMechanism, VersionIncompatible, CorruptedProvenance, PartialResearchVersion) maps to a stable HTTP status (docs/API_CONTRACT_v1.md:90-92, :100-110) — the full taxonomy table is in API Reference above. The contract states the service never returns a silent best-effort or fabricated result in place of one of these.

docs/API_CONTRACT_v1.md:90-92, :100-110

Idempotency & snapshot_id

The same request against the same version quad always returns an identical snapshot_id (docs/API_CONTRACT_v1.md:96, "Guarantees") — snapshot_id = sha256(identity + version triple + args + resolved profile) (docs/API_CONTRACT_v1.md:48), so it is safe to use as a cache key or a client-side dedupe key.

docs/API_CONTRACT_v1.md:48, :96

Bulk requests

POST /compose/bulk takes an items array (up to N, N unstated in the contract) and returns {mode, job, count, results[]} (docs/API_CONTRACT_v1.md:50-59) — see API Reference above for the full shape. The envelope is sync today; when async support lands, the same envelope switches to mode:"async" with a job object instead of breaking existing clients (docs/API_CONTRACT_v1.md:61-62).

docs/API_CONTRACT_v1.md:50-62