{"openapi":"3.1.0","info":{"title":"Émile API","description":"\n# Émile API\n\nPublic API for the Émile wine cellar app.\n\n## Authentication\n\nTwo methods are supported:\n\n**1. API Key** (recommended for external clients: MCP servers, scripts, ChatGPT custom connectors)\n\nSend your key in the `X-API-Key` header OR as `Authorization: Bearer emile_live_...`.\n\nGenerate keys from your iOS app: **Profil → Paramètres → Clés API**.\n\n**2. Supabase JWT** (used by the official iOS app — internal)\n\nSend `Authorization: Bearer <jwt>`.\n\n## Rate Limits\n\nAll endpoints are rate-limited per user. See each endpoint's documentation for specific limits.\nOn exceeded: HTTP 429 with `Retry-After` header and `X-RateLimit-*` informational headers.\n\n| Endpoint | Limit |\n|---|---|\n| `POST /scan` | 10/min, 50/day |\n| `POST /me/scan:batch` | 5/hour |\n| `POST /recommendations` | 20/min, 100/day |\n| `POST /cellars/{id}/bottles` | 60/min, 500/day |\n| `POST /me/bottles:batch` | 10/hour, 50/day |\n| `POST /me/keys` | 5/hour, 20/day |\n","version":"1.0"},"paths":{"/wines/search":{"get":{"tags":["wines"],"summary":"Search Wines","description":"Fuzzy search wines by name and optional producer using pg_trgm.","operationId":"search_wines_wines_search_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Wine name to search","title":"Q"},"description":"Wine name to search"},{"name":"producer","in":"query","required":false,"schema":{"type":"string","description":"Producer / domaine","default":"","title":"Producer"},"description":"Producer / domaine"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wines":{"post":{"tags":["wines"],"summary":"Manually create a wine reference","description":"Used when neither the scan nor the text search surface the wine. Calls the same LLM enrichment used by scan to fill in grape varieties, food pairings, region, etc. If the LLM is unavailable the raw user input is inserted as-is so the user never ends up in a dead end. The created wine is public (visible to all users), tagged via `source` so we can audit / clean later.","operationId":"create_wine_wines_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WineCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/scan":{"post":{"tags":["scan"],"summary":"Scan Label","description":"Upload a wine label image → OCR via GPT-4o → fuzzy match → LLM enrichment if no match.\nReturns: { extracted, matches, llm_enriched }","operationId":"scan_label_scan_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_scan_label_scan_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cellars":{"get":{"tags":["cellars"],"summary":"List Cellars","description":"List all cellars for the authenticated user, with available bottle count.","operationId":"list_cellars_cellars_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["cellars"],"summary":"Create Cellar","description":"Create a new cellar for the authenticated user.","operationId":"create_cellar_cellars_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CellarCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cellars/{cellar_id}":{"patch":{"tags":["cellars"],"summary":"Update Cellar","description":"Update a cellar's metadata (only fields in the allowlist).","operationId":"update_cellar_cellars__cellar_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"cellar_id","in":"path","required":true,"schema":{"type":"string","title":"Cellar Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CellarUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["cellars"],"summary":"Delete Cellar","description":"Delete a cellar (only if it belongs to the authenticated user).","operationId":"delete_cellar_cellars__cellar_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"cellar_id","in":"path","required":true,"schema":{"type":"string","title":"Cellar Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cellars/{cellar_id}/bottles":{"get":{"tags":["bottles"],"summary":"List Bottles","operationId":"list_bottles_cellars__cellar_id__bottles_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"cellar_id","in":"path","required":true,"schema":{"type":"string","title":"Cellar Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["bottles"],"summary":"Add Bottle","description":"Create individual bottle rows. quantity=N creates N rows (1 per physical bottle).\nReturns a list of created bottles.","operationId":"add_bottle_cellars__cellar_id__bottles_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"cellar_id","in":"path","required":true,"schema":{"type":"string","title":"Cellar Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BottleCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cellars/{cellar_id}/bottles/{bottle_id}":{"patch":{"tags":["bottles"],"summary":"Update Bottle","operationId":"update_bottle_cellars__cellar_id__bottles__bottle_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"cellar_id","in":"path","required":true,"schema":{"type":"string","title":"Cellar Id"}},{"name":"bottle_id","in":"path","required":true,"schema":{"type":"string","title":"Bottle Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BottleUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["bottles"],"summary":"Delete Bottle","operationId":"delete_bottle_cellars__cellar_id__bottles__bottle_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"cellar_id","in":"path","required":true,"schema":{"type":"string","title":"Cellar Id"}},{"name":"bottle_id","in":"path","required":true,"schema":{"type":"string","title":"Bottle Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cellars/{cellar_id}/bottles/{bottle_id}/photo":{"post":{"tags":["bottles"],"summary":"Upload Location Photo","description":"Upload a photo of the bottle's physical location in the cellar.\nStores the image in Supabase Storage and updates location_photo_url on the bottle.\nMax file size: 10 MB.","operationId":"upload_location_photo_cellars__cellar_id__bottles__bottle_id__photo_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"cellar_id","in":"path","required":true,"schema":{"type":"string","title":"Cellar Id"}},{"name":"bottle_id","in":"path","required":true,"schema":{"type":"string","title":"Bottle Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_location_photo_cellars__cellar_id__bottles__bottle_id__photo_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/recommendations":{"post":{"tags":["recommendations"],"summary":"Recommend","description":"Pre-score bottles deterministically, then LLM-rerank top when a dish is set.","operationId":"recommend_recommendations_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecommendationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/profile":{"get":{"tags":["profile"],"summary":"Get Profile","description":"Return the authenticated user's profile.\n\n`email` comes from the JWT claim (always fresh — Supabase reissues the\nJWT on email change). `first_name` / `last_name` come from the profiles\ntable. If the row is missing (rare: pre-trigger account), it is created\non the fly so the endpoint always returns 200.","operationId":"get_profile_me_profile_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["profile"],"summary":"Patch Profile","description":"Update the authenticated user's profile (partial update).\n\n- Absent fields are left unchanged (partial update semantics).\n- `email` always comes from the JWT; it is never modified by PATCH.\n- An empty body `{}` is treated as a no-op and returns the current\n  profile (200).","operationId":"patch_profile_me_profile_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/account":{"delete":{"tags":["profile"],"summary":"Delete account","description":"Permanently deletes the authenticated user's account and all associated data (bottles, cellars, api_keys, auth.users). Irreversible.","operationId":"delete_account_me_account_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/keys":{"post":{"tags":["API Keys"],"summary":"Create a new API key","description":"Returns the plaintext key ONCE. Subsequent reads only expose the prefix.","operationId":"create_key_me_keys_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["API Keys"],"summary":"List API keys (no plaintext)","description":"Returns prefix + metadata only. Plaintext keys are never re-exposed.","operationId":"list_keys_me_keys_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Keys Me Keys Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/keys/{key_id}":{"delete":{"tags":["API Keys"],"summary":"Revoke an API key","description":"Sets revoked_at = now(). The key is immediately unusable.","operationId":"revoke_key_me_keys__key_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/bottles:batch":{"post":{"tags":["Bottles"],"summary":"Batch-create bottles (text mode)","description":"Creates up to 100 bottles in one call.  Each bottle is processed\nindependently — partial success is supported.\n\n**Rate limit**: 10/hour, 50/day.\n\n**Returns**: per-bottle status.  Each entry is `{\"status\": \"created\", \"id\": \"...\"}` or\n`{\"status\": \"error\", \"error\": \"...\"}`.  Aggregate counts in `created` and `errored`.","operationId":"batch_create_bottles_me_bottles_batch_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchBottlesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Batch Create Bottles Me Bottles Batch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/scan:batch":{"post":{"tags":["Scan"],"summary":"Batch-scan up to 10 wine label images","description":"Each image is OCR'd + enriched + matched against wine_references.\nReturns per-image status: matched (confidence > 0.6), partial (lower confidence), failed (error).\nHeavy LLM-cost endpoint — strict rate limit applies.\n\n**Rate limit**: 5/hour. Max 10 images per batch.","operationId":"scan_batch_me_scan_batch_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_scan_batch_me_scan_batch_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Scan Batch Me Scan Batch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/privacy":{"get":{"tags":["legal"],"summary":"Privacy Policy","operationId":"privacy_policy_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/terms":{"get":{"tags":["legal"],"summary":"Terms Of Service","operationId":"terms_of_service_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/auth/callback":{"get":{"tags":["auth"],"summary":"Auth Callback","operationId":"auth_callback_auth_callback_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/oauth/authorize":{"get":{"tags":["OAuth"],"summary":"Authorize","description":"Validates the OAuth request shape, then 302-redirects to the\nconsent UI which lives on app.emile.wine. Errors that occur BEFORE\nredirect_uri validation return a direct 400 (never redirect to an\nuntrusted client URL — RFC 6749 § 4.1.2.1).","operationId":"authorize_oauth_authorize_get","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string","title":"Redirect Uri"}},{"name":"response_type","in":"query","required":false,"schema":{"type":"string","default":"code","title":"Response Type"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string","default":"emile.full","title":"Scope"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"code_challenge","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge"}},{"name":"code_challenge_method","in":"query","required":false,"schema":{"type":"string","default":"S256","title":"Code Challenge Method"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/authorize/grant":{"post":{"tags":["OAuth"],"summary":"Authorize Grant","description":"Called by the consent UI after the user clicks \"Autoriser\". The\nSupabase JWT identifies which user is granting consent. Returns the\nredirect URL with the freshly-issued code.","operationId":"authorize_grant_oauth_authorize_grant_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/token":{"post":{"tags":["OAuth"],"summary":"Token","operationId":"token_oauth_token_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_token_oauth_token_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/revoke":{"post":{"tags":["OAuth"],"summary":"Revoke","description":"Idempotent. RFC 7009 § 2.2: even unknown tokens return 200. We\nstill authenticate the client to prevent unauthenticated DoS of\nother people's tokens (though the impact would be 0 since revoking\nis idempotent — we just rate-limit anyway).","operationId":"revoke_oauth_revoke_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_revoke_oauth_revoke_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/introspect":{"post":{"tags":["OAuth"],"summary":"Introspect","description":"Resolves an access token to its claims for the MCP remote\nserver. Protected by a shared secret (env `OAUTH_INTROSPECT_SECRET`)\nrather than client_credentials because the introspector is our own\ninternal service, not an OAuth client.","operationId":"introspect_oauth_introspect_post","parameters":[{"name":"X-Introspect-Secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Introspect-Secret"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_introspect_oauth_introspect_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntrospectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/clients/{client_id}":{"get":{"tags":["OAuth"],"summary":"Get Client Public","description":"Consent UI calls this (no auth) to know what name/logo to show.\nClient names + logos are public by nature — they appear in the\nconsent screen everyone sees.","operationId":"get_client_public_oauth_clients__client_id__get","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientPublicResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/register":{"post":{"tags":["OAuth"],"summary":"Register","description":"Dynamic Client Registration — RFC 7591. Unauthenticated by\ndesign (stores need to register themselves without a pre-existing\naccount on Émile), tightly rate-limited (5/h per IP) to avoid\ndrive-by client spam.\n\nAll clients created here are public (`is_dynamic=true`, no secret)\n— they must use PKCE for the auth code exchange. Confidential\nclients are still possible but are curated by an admin via the\nDB directly (no public endpoint exposes them).","operationId":"register_oauth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/knowledge/search":{"post":{"tags":["RAG"],"summary":"Search","description":"Embed the query, then cosine-search knowledge_chunks. Returns {\"results\": [...]}.","operationId":"search_knowledge_search_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Search Knowledge Search Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/knowledge/search":{"post":{"tags":["RAG internal"],"summary":"Internal Search","description":"Internal-traffic search (same logic as /knowledge/search) auth'd via\nX-Internal-Secret rather than a user JWT. Used by the n8n content pipeline\nso it doesn't need to impersonate a real Émile user.","operationId":"internal_search_internal_knowledge_search_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Internal-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Internal Search Internal Knowledge Search Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/knowledge/search_grounded":{"post":{"tags":["RAG internal"],"summary":"Internal Search Grounded","description":"RAG + Exa web search merged into a single result list.\n\nUsed by the n8n content pipeline when curated RAG alone is too sparse\n(most pillars except 'vins'/'appellations' have no FR content). Both\nlegs fire in parallel; Exa is best-effort (returns [] on key missing or\nupstream error) so RAG outages don't take the whole call down.","operationId":"internal_search_grounded_internal_knowledge_search_grounded_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Internal-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchQuery"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Internal Search Grounded Internal Knowledge Search Grounded Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/knowledge/bulk_insert":{"post":{"tags":["RAG internal"],"summary":"Bulk Insert","description":"Insert pre-embedded chunks. The ingestion script computes embeddings\nin bulk (cheaper than per-query) and pushes them here.","operationId":"bulk_insert_internal_knowledge_bulk_insert_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Internal-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkInsertRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Bulk Insert Internal Knowledge Bulk Insert Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/knowledge/by_source/{source}":{"delete":{"tags":["RAG internal"],"summary":"Delete By Source","description":"Wipe all knowledge_chunks rows for a given source name. Used by the\ningestion script before re-running a source — bulk_insert is not\nidempotent on (source, source_url, content_text), so a re-run would\notherwise duplicate everything that was already ingested.","operationId":"delete_by_source_internal_knowledge_by_source__source__delete","parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}},{"name":"x-internal-secret","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Internal-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete By Source Internal Knowledge By Source  Source  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/blog/render":{"post":{"tags":["Blog internal"],"summary":"Render Blog Article","description":"Returns {html, file_path, blog_url, slug}.","operationId":"render_blog_article_internal_blog_render_post","parameters":[{"name":"x-internal-secret","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Internal-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Render Blog Article Internal Blog Render Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/dashboard":{"get":{"tags":["Dashboard internal"],"summary":"Dashboard","operationId":"dashboard_internal_dashboard_get","parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"html","title":"Format"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health":{"get":{"summary":"Health","operationId":"health_health_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Health","operationId":"health_health_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["OAuth"],"summary":"Oauth Authorization Server Metadata","description":"RFC 8414 — Authorization Server Metadata. Stores hit this URL\nduring onboarding to learn our endpoints, grant types, scopes,\nand PKCE support.","operationId":"oauth_authorization_server_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["OAuth"],"summary":"Oauth Protected Resource Metadata","description":"RFC 9728 — Protected Resource Metadata. Tells MCP clients\nwhich authorization server protects this resource. Once the MCP\nremote container lives at mcp.emile.wine (Phase C5) it will host\nits OWN copy of this endpoint pointing back at api.emile.wine\nas the authorization server.","operationId":"oauth_protected_resource_metadata__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"BatchBottlesRequest":{"properties":{"bottles":{"items":{"$ref":"#/components/schemas/BottleSpec"},"type":"array","maxItems":100,"minItems":1,"title":"Bottles"}},"type":"object","required":["bottles"],"title":"BatchBottlesRequest"},"Body_introspect_oauth_introspect_post":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"Body_introspect_oauth_introspect_post"},"Body_revoke_oauth_revoke_post":{"properties":{"token":{"type":"string","title":"Token"},"token_type_hint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type Hint"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Secret"}},"type":"object","required":["token"],"title":"Body_revoke_oauth_revoke_post"},"Body_scan_batch_me_scan_batch_post":{"properties":{"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_scan_batch_me_scan_batch_post"},"Body_scan_label_scan_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_scan_label_scan_post"},"Body_token_oauth_token_post":{"properties":{"grant_type":{"type":"string","title":"Grant Type"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Secret"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"redirect_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Redirect Uri"},"code_verifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Verifier"},"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token"}},"type":"object","required":["grant_type"],"title":"Body_token_oauth_token_post"},"Body_upload_location_photo_cellars__cellar_id__bottles__bottle_id__photo_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_location_photo_cellars__cellar_id__bottles__bottle_id__photo_post"},"BottleCreate":{"properties":{"wine_reference_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wine Reference Id"},"vintage":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Vintage"},"quantity":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Quantity","default":1},"location_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Text"},"location_photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Photo Url"},"purchase_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Purchase Price"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"BottleCreate"},"BottleSpec":{"properties":{"cellar_id":{"type":"string","format":"uuid","title":"Cellar Id"},"wine_reference_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wine Reference Id"},"vintage":{"anyOf":[{"type":"integer","maximum":2100.0,"minimum":1900.0},{"type":"null"}],"title":"Vintage"},"quantity":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Quantity","default":1},"location_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Text"},"location_photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Photo Url"},"purchase_price":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Purchase Price"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["cellar_id"],"title":"BottleSpec"},"BottleUpdate":{"properties":{"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"location_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Text"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"purchase_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Purchase Price"},"cellar_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cellar Id"}},"type":"object","title":"BottleUpdate"},"BulkInsertRequest":{"properties":{"chunks":{"items":{"$ref":"#/components/schemas/KnowledgeChunk"},"type":"array","title":"Chunks"}},"type":"object","required":["chunks"],"title":"BulkInsertRequest","description":"Wrapper for the bulk_insert payload — keeps the door open for\nfuture top-level metadata (batch_id, source_run, etc.) without breaking\nthe wire format."},"CellarCreate":{"properties":{"name":{"type":"string","title":"Name","default":"Ma cave"},"location_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Description"},"capacity_hint":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity Hint"},"storage_type":{"type":"string","title":"Storage Type","default":"cave"}},"type":"object","title":"CellarCreate"},"CellarUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"location_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Description"},"capacity_hint":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity Hint"},"storage_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Type"}},"type":"object","title":"CellarUpdate"},"ClientPublicResponse":{"properties":{"client_id":{"type":"string","title":"Client Id"},"name":{"type":"string","title":"Name"},"logo_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Uri"},"scope_description":{"type":"string","title":"Scope Description","default":"Accès complet à ta cave Émile (lecture + écriture)"}},"type":"object","required":["client_id","name"],"title":"ClientPublicResponse"},"CreateKeyRequest":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name"}},"type":"object","required":["name"],"title":"CreateKeyRequest"},"CreateKeyResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"key":{"type":"string","title":"Key"},"prefix":{"type":"string","title":"Prefix"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","key","prefix","created_at"],"title":"CreateKeyResponse"},"FilterCriteriaPayload":{"properties":{"colors":{"items":{"type":"string"},"type":"array","title":"Colors","default":[]},"window":{"type":"string","enum":["all","drinkable","aging","passed"],"title":"Window","default":"all"},"statuses":{"items":{"type":"string"},"type":"array","title":"Statuses","default":[]},"regions":{"items":{"type":"string"},"type":"array","title":"Regions","default":[]},"countries":{"items":{"type":"string"},"type":"array","title":"Countries","default":[]},"grapes":{"items":{"type":"string"},"type":"array","title":"Grapes","default":[]},"price_min":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Price Min"},"price_max":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Price Max"},"full_text":{"type":"string","title":"Full Text","default":""}},"type":"object","title":"FilterCriteriaPayload"},"GrantRequest":{"properties":{"client_id":{"type":"string","title":"Client Id"},"redirect_uri":{"type":"string","title":"Redirect Uri"},"scope":{"type":"string","title":"Scope","default":"emile.full"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"code_challenge":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge"},"code_challenge_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge Method","default":"S256"}},"type":"object","required":["client_id","redirect_uri"],"title":"GrantRequest"},"GrantResponse":{"properties":{"redirect_to":{"type":"string","title":"Redirect To"}},"type":"object","required":["redirect_to"],"title":"GrantResponse","description":"Returned to the consent UI's fetch() call. The browser then\ndoes `window.location = redirect_to` to bounce back to the store.\nServer-side redirect would also work but it requires the consent\nUI to be on the same origin, which it isn't."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IntrospectionResponse":{"properties":{"active":{"type":"boolean","title":"Active"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"},"exp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Exp","description":"Unix epoch seconds"}},"type":"object","required":["active"],"title":"IntrospectionResponse"},"KnowledgeChunk":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"source":{"type":"string","title":"Source"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"content_text":{"type":"string","minLength":10,"title":"Content Text"},"content_embedding":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"type":"null"}],"title":"Content Embedding"},"language":{"type":"string","enum":["fr","en"],"title":"Language"},"pillar":{"type":"string","enum":["vins","viticulture","cave","sommellerie","accord","appellations"],"title":"Pillar"},"topic":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Topic"},"token_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Token Count"}},"type":"object","required":["source","content_text","language","pillar"],"title":"KnowledgeChunk","description":"A single ingested chunk with metadata."},"ProfileUpdate":{"properties":{"first_name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Last Name"},"phone":{"anyOf":[{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$|^$"},{"type":"null"}],"title":"Phone"}},"type":"object","title":"ProfileUpdate"},"RecommendationRequest":{"properties":{"maturity":{"type":"boolean","title":"Maturity","default":false},"food":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Food"},"filters":{"anyOf":[{"$ref":"#/components/schemas/FilterCriteriaPayload"},{"type":"null"}]}},"type":"object","title":"RecommendationRequest"},"RegisterRequest":{"properties":{"client_name":{"type":"string","maxLength":120,"minLength":1,"title":"Client Name"},"redirect_uris":{"items":{"type":"string"},"type":"array","maxItems":10,"minItems":1,"title":"Redirect Uris"},"grant_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Grant Types"},"response_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Response Types"},"scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"},"logo_uri":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Logo Uri"}},"type":"object","required":["client_name","redirect_uris"],"title":"RegisterRequest","description":"RFC 7591 § 2 — a subset of the client metadata fields. The\nspec allows many more (jwks, contacts, tos_uri, …) — we accept\nthem silently but only persist what we know how to handle, so\naware stores remain conformant."},"RegisterResponse":{"properties":{"client_id":{"type":"string","title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Secret"},"client_id_issued_at":{"type":"integer","title":"Client Id Issued At"},"client_secret_expires_at":{"type":"integer","title":"Client Secret Expires At","default":0},"client_name":{"type":"string","title":"Client Name"},"redirect_uris":{"items":{"type":"string"},"type":"array","title":"Redirect Uris"},"grant_types":{"items":{"type":"string"},"type":"array","title":"Grant Types"},"response_types":{"items":{"type":"string"},"type":"array","title":"Response Types"},"scope":{"type":"string","title":"Scope"},"logo_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Uri"},"token_endpoint_auth_method":{"type":"string","title":"Token Endpoint Auth Method","default":"client_secret_post"}},"type":"object","required":["client_id","client_id_issued_at","client_name","redirect_uris","grant_types","response_types","scope"],"title":"RegisterResponse","description":"RFC 7591 § 3.2.1. `client_secret_expires_at = 0` means it\nnever expires; the field is mandatory when a secret is returned."},"RenderRequest":{"properties":{"markdown":{"type":"string","minLength":50,"title":"Markdown","description":"The article markdown body, exactly as written into the GDoc."},"language":{"type":"string","pattern":"^(fr|en)$","title":"Language"},"slug":{"type":"string","minLength":1,"title":"Slug","description":"kebab-case slug for the URL/file path."},"pillar":{"type":"string","minLength":1,"title":"Pillar","description":"One of: vins, viticulture, cave, sommellerie, accord, appellations."},"target_keyword":{"type":"string","minLength":1,"title":"Target Keyword","description":"The SEO keyword the article targets."},"date_iso":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","title":"Date Iso","description":"YYYY-MM-DD publication date."},"description":{"type":"string","maxLength":320,"minLength":20,"title":"Description","description":"Meta description (~155 chars ideal)."},"equivalent_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Equivalent Url","description":"Full URL of the article in the other language, for hreflang cross-linking."},"equivalent_lang":{"anyOf":[{"type":"string","pattern":"^(fr|en)$"},{"type":"null"}],"title":"Equivalent Lang"}},"type":"object","required":["markdown","language","slug","pillar","target_keyword","date_iso","description"],"title":"RenderRequest","description":"Inputs to /internal/blog/render. n8n maps Notion properties + the\nGDoc markdown body straight onto this schema."},"SearchQuery":{"properties":{"query":{"type":"string","minLength":2,"title":"Query"},"language":{"type":"string","enum":["fr","en"],"title":"Language","default":"fr"},"pillars":{"anyOf":[{"items":{"type":"string","enum":["vins","viticulture","cave","sommellerie","accord","appellations"]},"type":"array"},{"type":"null"}],"title":"Pillars"},"top_k":{"type":"integer","maximum":20.0,"minimum":1.0,"title":"Top K","default":8}},"type":"object","required":["query"],"title":"SearchQuery","description":"Input for /knowledge/search."},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"Bearer"},"expires_in":{"type":"integer","title":"Expires In"},"refresh_token":{"type":"string","title":"Refresh Token"},"scope":{"type":"string","title":"Scope"}},"type":"object","required":["access_token","expires_in","refresh_token","scope"],"title":"TokenResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WineCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"producer":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Producer"},"appellation":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Appellation"},"region":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Region"},"country":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Country"},"color":{"anyOf":[{"type":"string","enum":["rouge","blanc","rosé","effervescent","liquoreux","other"]},{"type":"null"}],"title":"Color"}},"type":"object","required":["name"],"title":"WineCreate","description":"User-provided wine reference. All fields except `name` are optional;\nthe LLM-enrichment step fills the gaps when configured.\n\n`color` is restricted to the same allowlist as the CHECK constraint\non `wine_references.color` so an invalid value is rejected at the\nedge instead of bubbling up as a Postgres error."}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}