{"openapi":"3.1.0","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"Cellarion API","version":"1.278.0","summary":"Read your cellar, take bottles out, post sensor readings.","description":"The stable REST API for **your own Cellarion data**: your cellars and the ones shared with you, bottles, racks, personal fields, statistics and notifications. You reach it with a personal API token. This page describes the version of the server it is served from; the raw OpenAPI 3.1 document is at [/api/openapi.json](/api/openapi.json).\n\n## Which interface should I use?\n\n| You are building | Use |\n|---|---|\n| A dashboard, a home-automation integration, a script | **This REST API**, starting with `GET /api/inventory` |\n| Something that should react when the cellar changes | The **event stream**, `GET /api/events/stream`, then refetch |\n| An AI assistant (Claude, ChatGPT and others) | **MCP**: see [Connect an AI assistant](/connect-ai) |\n| A temperature or humidity sensor | **Climate ingest**, `POST /api/climate/ingest` |\n\nThe API gives you your own data. It does not expose Cellarion's AI features (label scanning, enrichment, the cellar chat); those stay in the app, and nothing in this reference spends AI credits.\n\n## Getting started\n\n1. **Create a token.** In the app, open *Settings → API tokens* and create one with the fewest scopes you need: `read` for a dashboard, plus `consume` if your integration takes bottles out. The token is shown once; store it like a password.\n2. **Pick the base URL.** `https://cellarion.app`, or your own server's address if you self-host. A self-hosted server can run an older version (see *Stability and versioning*).\n3. **Make a first call.**\n\n   ```bash\n   export CELLARION_TOKEN=cel_…\n   curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/auth/whoami\"\n   curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/inventory\"\n   ```\n\n4. **Or use a ready-made client.**\n   - **Python:** [pycellarion](https://pypi.org/project/pycellarion/) (`pip install pycellarion`), the async client behind the Home Assistant integration.\n   - **Home Assistant:** [Cellarion for Home Assistant](https://github.com/jagduvi1/ha-cellarion), installable from HACS.\n\n   ```python\n   import aiohttp\n   from pycellarion import CellarionClient\n\n   async with aiohttp.ClientSession() as session:\n       client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n       result = await client.get_inventory()\n       print(len(result.data[\"bottles\"]), \"bottles\")\n   ```\n\n## Authentication and scopes\n\nSend the token in the `Authorization` header: `Authorization: Bearer cel_…`. Never put it in a URL, where it ends up in logs and browser history.\n\n| Scope | Allows |\n|---|---|\n| `read` | Reading your cellars and the ones shared with you: bottles, racks, personal fields, statistics, notifications, and the event stream |\n| `consume` | Taking bottles out of the cellar |\n| `climate` | Posting sensor readings. Device tokens get it when you add a climate device in the app |\n| `write` | AI assistants over MCP only; it opens no REST operation |\n\nA request outside the token's scopes answers `403`. An unknown or revoked token answers `401`.\n\n## Errors\n\nEvery error is JSON: `{\"error\": \"A human-readable message\"}`, sometimes with a machine-readable `code`.\n\n| Status | Meaning | What to do |\n|---|---|---|\n| `400` | The request is malformed | Fix the request |\n| `401` | No token, or it is invalid or revoked | Ask the user for a new token; retrying will not help |\n| `403` | The token lacks the scope, or the user lacks the role in that cellar | Ask for a token with the right scope |\n| `404` | Not found, or not visible to this user | Check the id |\n| `409` | The state moved on, e.g. the bottle already left the cellar | Refetch before trying again |\n| `429` | Rate limited | Wait `Retry-After` seconds |\n| `5xx` | Server trouble | Retry later, with backoff |\n\n## Rate limits\n\nEvery answer carries `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (seconds until the window resets) and `RateLimit-Policy` (e.g. `200;w=900`: 200 requests per 900 seconds). A `429` also carries `Retry-After`. Today that is about 200 reads per 15 minutes per address, 60 writes per 15 minutes, and 60 inventory reads per 10 minutes per user. These numbers can change: read the headers rather than hard-coding them.\n\n## Caching with ETag\n\n`GET /api/inventory` answers with an `ETag`. Send it back as `If-None-Match`; when nothing changed you get `304 Not Modified` with no body, at almost no cost on either side. A good pattern is to poll the inventory every few minutes with `If-None-Match`, or to refetch when the event stream says something changed.\n\n## Pagination\n\n`GET /api/bottles` takes `limit` (1 to 200, default 30) and `offset` (default 0). The answer gives `total`, `count`, `limit` and `skip` (the offset of this page); page until `skip + count` reaches `total`. For the whole cellar at once, use `GET /api/inventory` instead.\n\n## Live updates\n\n`GET /api/events/stream` is a Server-Sent Events stream. It sends `stats_changed`, `notification` or `climate` when your data changes: it says *that* something changed, not what, so refetch what you show. A `: hb` comment arrives every 25 seconds; reconnect with backoff after 90 seconds of silence. Authenticate with the `Authorization` header (a browser `EventSource` cannot send headers; use a client that can).\n\n## Revoking a token\n\nWhen a user removes your integration, revoke its token with the token itself: `DELETE /api/tokens/self`. Users can also revoke any token under *Settings → API tokens*.\n\n## Stability and versioning\n\nWhat is in this reference is **stable**:\n\n- A documented endpoint, parameter or field keeps its name, type and meaning.\n- New endpoints, optional parameters, fields and enum values can appear at any time. Ignore what you don't know.\n- A removal or breaking change is announced in the [release notes](https://github.com/jagduvi1/Cellarion/releases) and on this page, its answers carry `Deprecation` and `Sunset` headers, and it takes effect **no sooner than six months later**.\n- `GET /api/inventory` carries a `schema` number that changes only with a breaking change.\n- Anything a token can reach that is **not** in this reference is internal and may change without notice. Please don't build on it. If you need something that isn't here, [open an issue](https://github.com/jagduvi1/Cellarion/issues).\n\n**Self-hosted servers** update on their owner's schedule, so they can be older than cellarion.app. Read a server's version from `GET /api/health`, or treat a `404` or `403` on a documented endpoint as \"not supported on this server\" and carry on without it.\n\n### Being removed\n\nUntil now a `read` token could also reach other `GET` routes under `/api/cellars`, `/api/bottles`, `/api/stats` and `/api/notifications` that are not in this reference, such as `/api/cellars/{id}` or `/api/stats/value-history`. They are deprecated from 1 November 2026: their answers carry `Deprecation`, `Sunset` and `Link` headers, and **from 1 May 2027 they answer tokens with `403`**. `GET /api/inventory` covers most of what they were used for; if something you need is missing, [open an issue](https://github.com/jagduvi1/Cellarion/issues) before then.\n\n## Security\n\n- Keep tokens secret: store them like passwords, never commit them, never put them in a URL.\n- Ask for the fewest scopes you need.\n- Revoke tokens you no longer use under *Settings → API tokens*, and revoke your integration's token when it is removed.\n- Call `https://` addresses only, so the token never crosses the network in the clear.\n- Image paths in answers point at the same server; fetch only paths of the documented shape.\n\n## Terms of use\n\nBuild whatever helps you and others enjoy a cellar. A few ground rules keep it fair:\n\n- **Personal and open-source use is welcome.** Use the API for yourself, or share what you build for free as open source, like the Home Assistant integration.\n- **Keep it free.** Don't charge for a product or service built on the Cellarion API, or for access to Cellarion data. Have a commercial idea? Talk to us first.\n- **If other people use your service,** keep it free and open source and say clearly that it isn't run by Cellarion. Never ask anyone for their Cellarion password, only for a token, which you keep safe and its owner can remove. You are responsible for the data your service receives.\n- **Play nice.** Stay within the rate limits, don't copy the shared wine registry in bulk (see the [Registry Data Terms](/terms)), and don't present your project as an official Cellarion product.\n\nWe may revoke a token or block access that breaks these terms or puts users or the service at risk. These terms cover cellarion.app: the owner of a self-hosted server sets their own, and the Cellarion software stays open source under the AGPL-3.0.","termsOfService":"https://cellarion.app/developers#section/Terms-of-use","license":{"name":"AGPL-3.0","identifier":"AGPL-3.0-only"},"contact":{"name":"Cellarion","url":"https://github.com/jagduvi1/Cellarion"}},"servers":[{"url":"https://cellarion.app"}],"security":[{"apiToken":[]}],"tags":[{"name":"Inventory","description":"Everything in your cellars in one cheap, cacheable read. Most integrations need only this."},{"name":"Live updates","description":"Know when to refetch, without polling."},{"name":"Cellars and bottles","description":"Cellars, filtered bottle lists, one bottle, racks."},{"name":"Drinking","description":"Take a bottle out of the cellar."},{"name":"Statistics","description":"Totals and drink-window counts for dashboards."},{"name":"Notifications","description":"Restock suggestions, drink-window reminders and other notices."},{"name":"Climate sensors","description":"Temperature and humidity from a sensor in your cellar."},{"name":"Images","description":"Thumbnails referenced by other answers."},{"name":"Tokens","description":"The token itself: who it belongs to, and revoking it."},{"name":"Server","description":"Health, version and this document."}],"paths":{"/api/inventory":{"get":{"operationId":"getInventory","tags":["Inventory"],"summary":"The whole inventory in one read","description":"Every bottle still in your cellars and in the cellars shared with you, each with its wine, its drink window, where it is (rack, shelf, row, slot) and its personal fields. **Start here**: one call replaces paging the bottle list, and an unchanged poll costs almost nothing.\n\nSend the `ETag` of the copy you hold as `If-None-Match`. When nothing changed you get `304` with no body. The tag moves when any of the data changes, and at least every 30 minutes.\n\nLimited to 60 requests per user per 10 minutes, 304s included. Poll every few minutes, or refetch when the event stream says something changed.","security":[{"apiToken":["read"]}],"parameters":[{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"The inventory.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"description":"Always `no-store`: the answer holds personal data.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Inventory"},"example":{"schema":1,"generatedAt":"2026-10-22T08:00:00.000Z","cellars":[{"id":"66f0a1b2c3d4e5f6a7b8c902","name":"Home","role":"owner"},{"id":"66f0a1b2c3d4e5f6a7b8c903","name":"Summer house","role":"editor"}],"racks":[{"id":"66f0a1b2c3d4e5f6a7b8c904","cellar":"66f0a1b2c3d4e5f6a7b8c902","name":"Wine fridge"}],"wines":{"66f0a1b2c3d4e5f6a7b8c905":{"name":"Château Exemple","producer":"Château Exemple","type":"red","colour":null,"country":"France","region":"Pauillac","appellation":"Pauillac","classification":null,"grapes":["Cabernet Sauvignon","Merlot"],"image":"/api/uploads/thumbs/processed/3f2b8c1e-4a5d-4e6f-9a7b-1c2d3e4f5a6b.webp.webp","profile":{"body":"full","acidity":"medium","sweetness":"dry","tannin":"high","flavors":["blackcurrant","cedar"],"foodPairings":["lamb"]}}},"bottles":[{"id":"66f0a1b2c3d4e5f6a7b8c906","wine":"66f0a1b2c3d4e5f6a7b8c905","pending":null,"cellar":"66f0a1b2c3d4e5f6a7b8c902","vintage":"2015","size":"750ml","price":350,"currency":"SEK","rating":92,"ratingScale":"100","notes":"For a birthday","purchaseDate":"2024-05-01","purchaseLocation":"Wine shop","addedAt":"2025-01-02T10:00:00.000Z","opened":false,"maturity":"peak","window":{"drinkFrom":2022,"peakFrom":2025,"peakUntil":2032,"drinkTo":2038},"location":{"rack":"66f0a1b2c3d4e5f6a7b8c904","rackName":"Wine fridge","position":5,"shelf":2,"row":1,"col":2},"place":null,"image":"/api/uploads/thumbs/processed/3f2b8c1e-4a5d-4e6f-9a7b-1c2d3e4f5a6b.webp.webp","personal":[{"key":"Closure","type":"text","unit":null,"value":"cork","level":"bottle","by":"alice"}]}]}}}},"304":{"description":"Nothing changed since that ETag. No body.","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -i -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/inventory\"\n\n# Later: send the ETag back; 304 means nothing changed\ncurl -i -H 'If-None-Match: W/\"…\"' -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/inventory\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    result = await client.get_inventory()\n    # later, with the tag of the copy you hold:\n    result = await client.get_inventory(etag=result.etag)\n    if result.not_modified:\n        pass  # your copy is current"}]}},"/api/events/stream":{"get":{"operationId":"streamEvents","tags":["Live updates"],"summary":"Server-sent events when your data changes","description":"A long-lived `text/event-stream` (Server-Sent Events). It says *that* something changed, not what: refetch what you show (for example `GET /api/inventory` with its ETag).\n\nOn connect: `retry: 5000`, then `event: ready`. Events: `stats_changed` (`data: {\"reason\": \"…\"}`), `notification` (`data: {\"id\": \"…\", \"type\": \"…\"}`) and `climate` (`data: {\"device\": \"…\"}`). Events are coalesced over about 2 seconds. A `: hb` comment arrives every 25 seconds; treat 90 seconds of silence as a lost connection and reconnect with backoff.\n\nA stream lasts at most 24 hours and closes when the token is revoked. At most 5 streams per user (429 beyond). New event names may appear; ignore the ones you don't know.","security":[{"apiToken":["read"]}],"responses":{"200":{"description":"The event stream.","content":{"text/event-stream":{"schema":{"type":"string"},"example":"retry: 5000\n\nevent: ready\ndata: {}\n\nevent: stats_changed\ndata: {\"reason\":\"bottle.consume\"}\n\n: hb\n\n"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -N -H \"Accept: text/event-stream\" -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/events/stream\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    async for event in client.events_stream():\n        if event != \"_connected\":\n            print(\"changed:\", event)"}]}},"/api/auth/whoami":{"get":{"operationId":"whoAmI","tags":["Tokens"],"summary":"Which account this token belongs to","description":"The account id only, no personal details. Useful to check that a new token belongs to the same account as the old one.","security":[{"apiToken":["read"]}],"responses":{"200":{"description":"The account id.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoAmI"},"example":{"id":"66f0a1b2c3d4e5f6a7b8c901"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/auth/whoami\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    account_id = await client.get_account_id()"}]}},"/api/cellars":{"get":{"operationId":"listCellars","tags":["Cellars and bottles"],"summary":"Your cellars and the ones shared with you","security":[{"apiToken":["read"]}],"responses":{"200":{"description":"The cellars, newest first.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CellarList"},"example":{"count":1,"cellars":[{"_id":"66f0a1b2c3d4e5f6a7b8c902","name":"Home","description":"The basement","user":"66f0a1b2c3d4e5f6a7b8c901","userRole":"owner","userColor":null,"createdAt":"2025-01-02T10:00:00.000Z","updatedAt":"2025-01-02T10:00:00.000Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/cellars\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    cellars = (await client.get_cellars())[\"cellars\"]"}]}},"/api/bottles":{"get":{"operationId":"listBottles","tags":["Cellars and bottles"],"summary":"Bottles in the cellars you own, filtered and paged","description":"Bottles in cellars you **own** (shared cellars are not included; `GET /api/inventory` covers both). By default only bottles still in the cellar. For everything at once, prefer `GET /api/inventory`.","security":[{"apiToken":["read"]}],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Offset"},{"name":"status","in":"query","schema":{"type":"string","enum":["active","all","ordered","drank","gifted","sold","other","consumed"],"default":"active"},"description":"`consumed` = drank, gifted, sold or other."},{"name":"maturity","in":"query","schema":{"type":"string","examples":["peak","late,declining"]},"description":"One or more of `not-ready`, `early`, `peak`, `late`, `declining`, `none`, comma-separated."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Words that must all appear (accents ignored) in the wine name, producer, grapes, country, region, appellation, type, notes or location."},{"name":"type","in":"query","schema":{"type":"string"},"description":"Wine type, e.g. `red`. Comma-separated for several."},{"name":"vintage","in":"query","schema":{"type":"string"},"description":"One or more vintages, comma-separated."},{"name":"sort","in":"query","schema":{"type":"string","enum":["createdAt","-createdAt","vintage","-vintage","price","-price","rating","-rating","name","-name","maturity","-maturity"],"default":"-createdAt"},"description":"A leading `-` sorts descending."}],"responses":{"200":{"description":"One page of bottles.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BottleList"},"example":{"bottles":{"count":1,"total":1,"limit":30,"skip":0,"items":[{"_id":"66f0a1b2c3d4e5f6a7b8c906","cellar":"66f0a1b2c3d4e5f6a7b8c902","wineDefinition":{"_id":"66f0a1b2c3d4e5f6a7b8c905","name":"Château Exemple","producer":"Château Exemple","type":"red","colour":null,"country":{"_id":"66f0a1b2c3d4e5f6a7b8c908","name":"France"},"region":{"_id":"66f0a1b2c3d4e5f6a7b8c909","name":"Pauillac"},"appellation":"Pauillac","classification":null,"grapes":[{"_id":"66f0a1b2c3d4e5f6a7b8c90a","name":"Cabernet Sauvignon","displayName":"Cabernet Sauvignon"}],"image":"/api/uploads/processed/3f2b8c1e-4a5d-4e6f-9a7b-1c2d3e4f5a6b.webp"},"pendingWineRequest":null,"vintage":"2015","bottleSize":"750ml","price":350,"currency":"SEK","rating":92,"ratingScale":"100","status":"active","location":"Basement, box 3","notes":"For a birthday","purchaseDate":"2024-05-01T00:00:00.000Z","purchaseLocation":"Wine shop","openedAt":null,"addedToCellarAt":"2025-01-02T10:00:00.000Z","createdAt":"2025-01-02T10:00:00.000Z","updatedAt":"2025-01-02T10:00:00.000Z","defaultImageUrl":null,"vintageImageUrl":null,"pendingImageUrl":null,"otherVintageImageUrl":null}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/bottles?maturity=peak&limit=10\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    peak = await client.get_peak_bottles(limit=10)\n    for bottle in peak[\"bottles\"][\"items\"]:\n        print(bottle[\"wineDefinition\"][\"name\"], bottle.get(\"vintage\"))"}]}},"/api/bottles/{bottleId}":{"get":{"operationId":"getBottle","tags":["Cellars and bottles"],"summary":"One bottle","security":[{"apiToken":["read"]}],"parameters":[{"$ref":"#/components/parameters/BottleId"}],"responses":{"200":{"description":"The bottle.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BottleDetail"},"example":{"bottle":{"_id":"66f0a1b2c3d4e5f6a7b8c906","cellar":"66f0a1b2c3d4e5f6a7b8c902","wineDefinition":{"_id":"66f0a1b2c3d4e5f6a7b8c905","name":"Château Exemple","producer":"Château Exemple","type":"red","colour":null,"country":{"_id":"66f0a1b2c3d4e5f6a7b8c908","name":"France"},"region":{"_id":"66f0a1b2c3d4e5f6a7b8c909","name":"Pauillac"},"appellation":"Pauillac","classification":null,"grapes":[{"_id":"66f0a1b2c3d4e5f6a7b8c90a","name":"Cabernet Sauvignon","displayName":"Cabernet Sauvignon"}],"image":"/api/uploads/processed/3f2b8c1e-4a5d-4e6f-9a7b-1c2d3e4f5a6b.webp"},"pendingWineRequest":null,"vintage":"2015","bottleSize":"750ml","price":350,"currency":"SEK","rating":92,"ratingScale":"100","status":"active","location":"Basement, box 3","notes":"For a birthday","purchaseDate":"2024-05-01T00:00:00.000Z","purchaseLocation":"Wine shop","openedAt":null,"addedToCellarAt":"2025-01-02T10:00:00.000Z","createdAt":"2025-01-02T10:00:00.000Z","updatedAt":"2025-01-02T10:00:00.000Z","defaultImageUrl":null,"vintageImageUrl":null,"pendingImageUrl":null,"otherVintageImageUrl":null},"userRole":"owner","rackInfo":{"rackId":"66f0a1b2c3d4e5f6a7b8c904","rackName":"Wine fridge","position":5},"defaultImageUrl":null,"vintageImageUrl":null,"pendingImageUrl":null,"otherVintageImageUrl":null}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/bottles/66f0a1b2c3d4e5f6a7b8c906\""},{"lang":"Python","label":"Python","source":"import json, urllib.request\n\nreq = urllib.request.Request(\n    \"https://cellarion.app/api/bottles/66f0a1b2c3d4e5f6a7b8c906\",\n    method=\"GET\",\n    headers={\"Authorization\": \"Bearer cel_…\"},\n)\nwith urllib.request.urlopen(req) as res:\n    answer = json.load(res)"}]}},"/api/bottles/{bottleId}/consume":{"post":{"operationId":"consumeBottle","tags":["Drinking"],"summary":"Take a bottle out of the cellar","description":"Marks the bottle drunk (or gifted, sold, other). Needs the `consume` scope and editor access to the cellar. Send `ifActive: true` so a bottle that already left the cellar is never overwritten.","security":[{"apiToken":["consume"]}],"parameters":[{"$ref":"#/components/parameters/BottleId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsumeRequest"},"example":{"reason":"drank","rating":4,"consumedRatingScale":"5","note":"With friends","ifActive":true}}}},"responses":{"200":{"description":"The bottle, now out of the cellar.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsumeResponse"},"example":{"bottle":{"_id":"66f0a1b2c3d4e5f6a7b8c906","cellar":"66f0a1b2c3d4e5f6a7b8c902","wineDefinition":{"_id":"66f0a1b2c3d4e5f6a7b8c905","name":"Château Exemple","producer":"Château Exemple","type":"red","colour":null,"country":{"_id":"66f0a1b2c3d4e5f6a7b8c908","name":"France"},"region":{"_id":"66f0a1b2c3d4e5f6a7b8c909","name":"Pauillac"},"appellation":"Pauillac","classification":null,"grapes":[{"_id":"66f0a1b2c3d4e5f6a7b8c90a","name":"Cabernet Sauvignon","displayName":"Cabernet Sauvignon"}],"image":"/api/uploads/processed/3f2b8c1e-4a5d-4e6f-9a7b-1c2d3e4f5a6b.webp"},"pendingWineRequest":null,"vintage":"2015","bottleSize":"750ml","price":350,"currency":"SEK","rating":92,"ratingScale":"100","status":"drank","location":"Basement, box 3","notes":"For a birthday","purchaseDate":"2024-05-01T00:00:00.000Z","purchaseLocation":"Wine shop","openedAt":null,"addedToCellarAt":"2025-01-02T10:00:00.000Z","createdAt":"2025-01-02T10:00:00.000Z","updatedAt":"2025-01-02T10:00:00.000Z","defaultImageUrl":null,"vintageImageUrl":null,"pendingImageUrl":null,"otherVintageImageUrl":null,"consumedAt":"2026-10-22T19:30:00.000Z","consumedReason":"drank"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X POST -H \"Content-Type: application/json\" \\\n  -d '{\"reason\": \"drank\", \"rating\": 4, \"consumedRatingScale\": \"5\", \"ifActive\": true}' \\\n  -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/bottles/66f0a1b2c3d4e5f6a7b8c906/consume\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    await client.consume_bottle(\"66f0a1b2c3d4e5f6a7b8c906\", reason=\"drank\", rating=4)"}]}},"/api/racks":{"get":{"operationId":"listRacks","tags":["Cellars and bottles"],"summary":"The racks in one cellar","description":"With a token, racks are read in summary form: send `summary=1`. Slots carry bottle ids only.","security":[{"apiToken":["read"]}],"parameters":[{"name":"cellar","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]}},{"name":"summary","in":"query","required":true,"schema":{"type":"string","const":"1"}}],"responses":{"200":{"description":"The racks.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RackList"},"example":{"racks":[{"_id":"66f0a1b2c3d4e5f6a7b8c904","cellar":"66f0a1b2c3d4e5f6a7b8c902","name":"Wine fridge","group":null,"type":"cabinet","rows":2,"cols":3,"typeConfig":{"shelfRows":[1,1]},"isModular":false,"slots":[{"position":5,"bottle":"66f0a1b2c3d4e5f6a7b8c906"}],"createdAt":"2025-01-02T10:00:00.000Z","updatedAt":"2025-01-02T10:00:00.000Z"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/racks?cellar=66f0a1b2c3d4e5f6a7b8c902&summary=1\""},{"lang":"Python","label":"Python","source":"import json, urllib.request\n\nreq = urllib.request.Request(\n    \"https://cellarion.app/api/racks?cellar=66f0a1b2c3d4e5f6a7b8c902&summary=1\",\n    method=\"GET\",\n    headers={\"Authorization\": \"Bearer cel_…\"},\n)\nwith urllib.request.urlopen(req) as res:\n    answer = json.load(res)"}]}},"/api/stats/overview":{"get":{"operationId":"getStatsOverview","tags":["Statistics"],"summary":"Statistics over the cellars you own","description":"Values in your preferred currency and rating scale. Cached for up to 30 minutes.","security":[{"apiToken":["read"]}],"responses":{"200":{"description":"The statistics.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsOverview"},"example":{"stats":{"overview":{"totalBottles":42,"totalConsumed":17,"uniqueWines":30,"totalCellars":1,"totalCountries":6,"totalGrapes":12,"totalValue":15420,"currency":"SEK","avgPrice":367.14,"avgRating":78,"targetRatingScale":"5","oldestVintage":2005,"newestVintage":2023,"bottlesDrunk":15,"bottlesGifted":2,"bottlesSold":0},"maturity":{"declining":1,"late":3,"peak":12,"early":9,"notReady":10,"noProfile":7},"pace":{"avgIntakePerYear":24,"avgOutputPerYear":18,"netPerYear":6,"runway":2},"byType":{"red":25,"white":12,"sparkling":5},"byCountry":[{"name":"France","count":20},{"name":"Italy","count":9}],"cellarBreakdown":[{"id":"66f0a1b2c3d4e5f6a7b8c902","name":"Home","bottleCount":42,"value":15420}],"topProducers":[{"name":"Château Exemple","count":4}],"urgencyLadder":[{"id":"66f0a1b2c3d4e5f6a7b8c907","cellarId":"66f0a1b2c3d4e5f6a7b8c902","name":"Exemple Blanc","producer":"Domaine Exemple","vintage":"2016","type":"white","price":210,"status":"late"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/stats/overview\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    stats = (await client.get_stats_overview())[\"stats\"]\n    print(stats[\"overview\"][\"totalBottles\"], \"bottles\")"}]}},"/api/notifications":{"get":{"operationId":"listNotifications","tags":["Notifications"],"summary":"Your 30 newest notifications","security":[{"apiToken":["read"]}],"responses":{"200":{"description":"The notifications.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationList"},"example":{"notifications":[{"_id":"66f0a1b2c3d4e5f6a7b8c90b","type":"restock_alert","title":"Restock Suggestion","message":"You just finished your last bottle similar to Château Exemple. Time to restock?","link":"/restock","read":false,"createdAt":"2026-10-21T18:00:00.000Z"}],"unreadCount":1}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/notifications\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    notes = await client.get_notifications()\n    print(notes[\"unreadCount\"], \"unread\")"}]}},"/api/notifications/unread-count":{"get":{"operationId":"getUnreadCount","tags":["Notifications"],"summary":"The unread count, cheaply","description":"Poll this instead of the list; fetch the list when `newestId` or `unreadCount` changes.","security":[{"apiToken":["read"]}],"responses":{"200":{"description":"The count and the newest id.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnreadCount"},"example":{"unreadCount":1,"newestId":"66f0a1b2c3d4e5f6a7b8c90b"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/notifications/unread-count\""},{"lang":"Python","label":"Python","source":"import json, urllib.request\n\nreq = urllib.request.Request(\n    \"https://cellarion.app/api/notifications/unread-count\",\n    method=\"GET\",\n    headers={\"Authorization\": \"Bearer cel_…\"},\n)\nwith urllib.request.urlopen(req) as res:\n    answer = json.load(res)"}]}},"/api/climate/ingest":{"post":{"operationId":"ingestClimate","tags":["Climate sensors"],"summary":"Post sensor readings","description":"For a temperature or humidity sensor in a cellar. Authenticate with the device token shown when you add a climate device in the app (it carries only the `climate` scope). Readings are validated one by one: the valid ones are kept, the others reported in `errors`. Wait `intervalS` seconds between posts.","security":[{"apiToken":["climate"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClimateIngestRequest"},"example":{"readings":[{"channel":"fridge","type":"temperature","value":12.4},{"channel":"fridge","type":"humidity","value":68}],"firmware":"esp32-1.0"}}}},"responses":{"202":{"description":"Readings received.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClimateIngestResponse"},"example":{"accepted":2,"rejected":0,"intervalS":300}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X POST -H \"Content-Type: application/json\" \\\n  -d '{\"readings\": [{\"channel\": \"fridge\", \"type\": \"temperature\", \"value\": 12.4}]}' \\\n  -H \"Authorization: Bearer $CELLARION_DEVICE_TOKEN\" \"https://cellarion.app/api/climate/ingest\""},{"lang":"Python","label":"Python","source":"import json, urllib.request\n\nreq = urllib.request.Request(\n    \"https://cellarion.app/api/climate/ingest\",\n    method=\"POST\",\n    headers={\"Authorization\": \"Bearer cel_…\", \"Content-Type\": \"application/json\"},\n    data=json.dumps({\"readings\": [{\"channel\": \"fridge\", \"type\": \"temperature\", \"value\": 12.4}]}).encode(),\n)\nwith urllib.request.urlopen(req) as res:\n    answer = json.load(res)"}]}},"/api/tokens/self":{"delete":{"operationId":"revokeOwnToken","tags":["Tokens"],"summary":"Revoke the token making this request","description":"Any personal token may revoke itself, whatever its scopes, for example when an integration is removed. Its event streams close too. A climate device token is retired by deleting the device in the app instead (403).","security":[{"apiToken":[]}],"responses":{"200":{"description":"Revoked.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRevoked"},"example":{"message":"Token revoked","id":"66f0a1b2c3d4e5f6a7b8c90c","name":"Home Assistant"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE -H \"Authorization: Bearer $CELLARION_TOKEN\" \"https://cellarion.app/api/tokens/self\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    revoked = await client.revoke_own_token()"}]}},"/api/health":{"get":{"operationId":"getHealth","tags":["Server"],"summary":"Is the server up, and which version is it","description":"No token needed. A self-hosted server may run an older version than cellarion.app: read `version`, or treat a 404 or 403 on a documented path as \"not supported here\" and carry on without it.","security":[],"responses":{"200":{"description":"Up.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"example":{"status":"ok","mongo":"connected","version":"1.275.0"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"Starting, stopping or without its database.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"example":{"status":"draining","mongo":"connected","version":"1.275.0"}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl \"https://cellarion.app/api/health\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    health = await client.get_health()\n    print(health[\"version\"])"}]}},"/api/uploads/thumbs/{folder}/{file}":{"get":{"operationId":"getThumbnail","tags":["Images"],"summary":"A bottle or wine thumbnail","description":"The `image` paths in `GET /api/inventory` point here. No token needed: the file names are random. Fetch only paths of exactly this shape.","security":[],"parameters":[{"name":"folder","in":"path","required":true,"schema":{"type":"string","enum":["processed","originals"]}},{"name":"file","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+\\.(png|jpe?g|webp)\\.webp$"}}],"responses":{"200":{"description":"A WebP image.","content":{"image/webp":{"schema":{"type":"string","contentMediaType":"image/webp"}}}},"404":{"description":"No such image.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"The thumbnail is being rendered; try again after `Retry-After` seconds."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -o label.webp \"https://cellarion.app/api/uploads/thumbs/processed/3f2b8c1e-4a5d-4e6f-9a7b-1c2d3e4f5a6b.webp.webp\""},{"lang":"Python","label":"Python","source":"import aiohttp\nfrom pycellarion import CellarionClient\n\nasync with aiohttp.ClientSession() as session:\n    client = CellarionClient(session, \"https://cellarion.app\", token=\"cel_…\")\n    body, content_type = await client.get_image(bottle[\"image\"])  # a path from the inventory"}]}},"/api/openapi.json":{"get":{"operationId":"getOpenApiDocument","tags":["Server"],"summary":"This document","description":"The OpenAPI 3.1 description of this server's public API, for its own version. No token needed.","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object","required":["openapi","info","paths"],"properties":{"openapi":{"type":"string","const":"3.1.0"},"info":{"type":"object"},"paths":{"type":"object"}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -o cellarion-openapi.json \"https://cellarion.app/api/openapi.json\""},{"lang":"Python","label":"Python","source":"import json, urllib.request\n\nwith urllib.request.urlopen(\"https://cellarion.app/api/openapi.json\") as res:\n    document = json.load(res)"}]}}},"components":{"securitySchemes":{"apiToken":{"type":"http","scheme":"bearer","bearerFormat":"cel_…","description":"A personal API token, created under Settings → API tokens. Send it as `Authorization: Bearer cel_…`; never in a URL.\n\nEach token has scopes; an operation lists the scope it needs:\n- `read`: read your cellars, bottles, racks, personal fields, statistics and notifications, and the event stream.\n- `consume`: take bottles out of the cellar.\n- `climate`: post sensor readings (device tokens, made when you add a climate device).\n- `write`: only for AI assistants over MCP; it opens no REST operation.\n\nA request the token's scopes don't cover answers 403. A revoked or unknown token answers 401."}},"parameters":{"BottleId":{"name":"bottleId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]}},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":30}},"Offset":{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0},"description":"Items to skip. `skip` is accepted as a synonym."},"IfNoneMatch":{"name":"If-None-Match","in":"header","schema":{"type":"string"},"description":"The `ETag` of the copy you hold, exactly as you received it."}},"headers":{"ETag":{"description":"A weak validator for this answer. Send it back as `If-None-Match`.","schema":{"type":"string","pattern":"^W/\".+\"$"}},"RateLimitLimit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"RateLimitRemaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"RateLimitReset":{"description":"Seconds until the window resets.","schema":{"type":"integer"}},"RateLimitPolicy":{"description":"The limit and the window in seconds, e.g. `60;w=600`.","schema":{"type":"string"}},"RetryAfter":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"responses":{"BadRequest":{"description":"The request is malformed, e.g. an invalid id or value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid ID"}}}},"Unauthorized":{"description":"No token, or the token is invalid or revoked. Ask the user for a new one; retrying will not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid token"}}}},"Forbidden":{"description":"The token lacks the scope this operation needs, or the caller lacks the role in that cellar. On a self-hosted server it can also mean the server is older than this operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"API token scope insufficient for this request"}}}},"NotFound":{"description":"No such resource visible to the caller, or a server older than this operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Bottle not found"}}}},"Conflict":{"description":"The state moved on, e.g. the bottle already left the cellar (`code: state_changed`) or is still on order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This bottle has already left the cellar","code":"state_changed"}}}},"TooManyRequests":{"description":"Rate limited. Wait `Retry-After` seconds.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Too many requests, please try again later"}}}}},"schemas":{"Error":{"type":"object","description":"Every error answers this shape. `error` is a human-readable message (English); `code`, where present, is a stable machine-readable reason.","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","description":"Present on some errors, e.g. `state_changed` on a consume with `ifActive`."}}},"Health":{"type":"object","required":["status","mongo","version"],"properties":{"status":{"type":"string","enum":["ok","degraded","draining"]},"mongo":{"type":"string","enum":["connected","disconnected"]},"version":{"type":"string","description":"The server's Cellarion version. A self-hosted server can be older than cellarion.app.","examples":["1.275.0"]}}},"WhoAmI":{"type":"object","required":["id"],"properties":{"id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"],"description":"The id of the account the token belongs to. Stable; use it to tell two accounts apart."}}},"Inventory":{"type":"object","description":"Every bottle still in the caller's cellars and in the cellars shared with them. Drunk, gifted, sold and on-order bottles are left out. Each wine appears once in `wines`; bottles reference it by id.","required":["schema","generatedAt","cellars","racks","wines","bottles"],"properties":{"schema":{"type":"integer","const":1,"description":"The shape version of this document. It changes only with a breaking change, announced as described under Stability."},"generatedAt":{"type":"string","format":"date-time"},"cellars":{"type":"array","items":{"$ref":"#/components/schemas/InventoryCellar"}},"racks":{"type":"array","items":{"$ref":"#/components/schemas/InventoryRack"}},"wines":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/InventoryWine"},"description":"Keyed by wine id."},"bottles":{"type":"array","items":{"$ref":"#/components/schemas/InventoryBottle"}}}},"InventoryCellar":{"type":"object","required":["id","name","role"],"properties":{"id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":["string","null"]},"role":{"type":"string","enum":["owner","editor","viewer"]}}},"InventoryRack":{"type":"object","required":["id","cellar","name"],"properties":{"id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"cellar":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":["string","null"]}}},"InventoryWine":{"type":"object","required":["name","producer","type","colour","country","region","appellation","classification","grapes","image","profile"],"properties":{"name":{"type":["string","null"]},"producer":{"type":["string","null"]},"type":{"type":["string","null"],"enum":["red","white","rosé","sparkling","dessert","fortified",null]},"colour":{"type":["string","null"],"enum":["red","white","rosé","sparkling","dessert","fortified",null],"description":"The colour of a sparkling, dessert or fortified wine (e.g. a sparkling rosé), or null."},"country":{"type":["string","null"]},"region":{"type":["string","null"]},"appellation":{"type":["string","null"]},"classification":{"type":["string","null"]},"grapes":{"type":"array","items":{"type":"string"},"description":"Display names, as on the label."},"image":{"type":["string","null"],"description":"A thumbnail path on this server (see Images), or null."},"profile":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TastingProfile"}],"description":"The stored tasting descriptors, or null when the wine has none."}}},"TastingProfile":{"type":"object","required":["body","acidity","sweetness","tannin","flavors","foodPairings"],"properties":{"body":{"type":["string","null"]},"acidity":{"type":["string","null"]},"sweetness":{"type":["string","null"]},"tannin":{"type":["string","null"]},"flavors":{"type":"array","items":{"type":"string"},"maxItems":12},"foodPairings":{"type":"array","items":{"type":"string"},"maxItems":12}}},"InventoryBottle":{"type":"object","required":["id","wine","pending","cellar","vintage","size","price","currency","rating","ratingScale","notes","purchaseDate","purchaseLocation","addedAt","opened","maturity","window","location","place","image","personal"],"properties":{"id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"wine":{"type":["string","null"],"pattern":"^[a-f0-9]{24}$","description":"A key of `wines`, or null while the bottle waits on a wine request."},"pending":{"type":["object","null"],"description":"The wine request this bottle waits on, when `wine` is null.","properties":{"name":{"type":["string","null"]},"producer":{"type":["string","null"]}}},"cellar":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"vintage":{"type":["string","null"],"examples":["2015","NV"]},"size":{"type":["string","null"],"examples":["750ml","1500ml"]},"price":{"type":["number","null"]},"currency":{"type":["string","null"],"examples":["SEK","EUR"]},"rating":{"type":["number","null"]},"ratingScale":{"type":["string","null"],"enum":["5","20","100",null],"description":"Set only beside a rating."},"notes":{"type":["string","null"]},"purchaseDate":{"type":["string","null"],"format":"date"},"purchaseLocation":{"type":["string","null"]},"addedAt":{"type":["string","null"],"format":"date-time"},"opened":{"type":"boolean","description":"True while the bottle is open (some glasses poured)."},"maturity":{"type":["string","null"],"enum":["not-ready","early","peak","late","declining",null],"description":"Where the bottle is in its drink window today, or null when it has no window."},"window":{"$ref":"#/components/schemas/DrinkWindow"},"location":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/RackLocation"}],"description":"The rack slot holding the bottle, or null when it is in no rack."},"place":{"type":["string","null"],"description":"The bottle's free-text location (\"Basement, box 3\")."},"image":{"type":["string","null"],"description":"The bottle card's picture as a thumbnail path on this server, or null."},"personal":{"type":"array","items":{"$ref":"#/components/schemas/PersonalEntry"}}}},"DrinkWindow":{"type":"object","description":"The bottle's own window when it has one, else the reviewed window for its wine and vintage. Each is a year or null.","required":["drinkFrom","peakFrom","peakUntil","drinkTo"],"properties":{"drinkFrom":{"type":["integer","null"],"minimum":1900,"maximum":2200},"peakFrom":{"type":["integer","null"],"minimum":1900,"maximum":2200},"peakUntil":{"type":["integer","null"],"minimum":1900,"maximum":2200},"drinkTo":{"type":["integer","null"],"minimum":1900,"maximum":2200}}},"RackLocation":{"type":"object","required":["rack","rackName","position","shelf","row","col"],"properties":{"rack":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"rackName":{"type":["string","null"]},"position":{"type":["integer","null"],"minimum":1,"description":"The 1-based slot number."},"shelf":{"type":["integer","null"],"minimum":1,"description":"Wine fridges (cabinet racks): 1 = the top shelf. Null on other rack types."},"row":{"type":["integer","null"],"minimum":1,"description":"Cabinets: the row within the shelf, 1 = resting on the shelf. Grids: the row from the top. Null on other types."},"col":{"type":["integer","null"],"minimum":1,"description":"The slot within the row, left to right. Null where the rack has no rows."}}},"PersonalEntry":{"type":"object","description":"A typed personal field on the bottle or its wine, as the bottle page shows it. Bottle-level entries come first.","required":["key","type","unit","value","level","by"],"properties":{"key":{"type":"string","examples":["Closure"]},"type":{"type":["string","null"],"enum":["text","integer","decimal","boolean","date","enum",null]},"unit":{"type":["string","null"]},"value":{"description":"A string, number or boolean, matching `type`."},"level":{"type":"string","enum":["bottle","wine"]},"by":{"type":["string","null"],"description":"The username of the member who wrote it."}}},"CellarList":{"type":"object","required":["count","cellars"],"properties":{"count":{"type":"integer","minimum":0},"cellars":{"type":"array","items":{"$ref":"#/components/schemas/Cellar"}}}},"Cellar":{"type":"object","required":["_id","name","user","userRole"],"properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"},"description":{"type":"string"},"user":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"],"description":"The owner's account id."},"userRole":{"type":"string","enum":["owner","editor","viewer"],"description":"The caller's role in this cellar."},"userColor":{"type":["string","null"],"description":"The colour the caller picked for this cellar, or null."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"WineSummary":{"type":"object","required":["_id","name"],"properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"},"producer":{"type":["string","null"]},"type":{"type":["string","null"],"enum":["red","white","rosé","sparkling","dessert","fortified",null]},"colour":{"type":["string","null"],"enum":["red","white","rosé","sparkling","dessert","fortified",null]},"country":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/NamedRef"}]},"region":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/NamedRef"}]},"appellation":{"type":["string","null"]},"classification":{"type":["string","null"]},"grapes":{"type":"array","items":{"$ref":"#/components/schemas/Grape"}},"image":{"type":["string","null"],"description":"The registry image URL, or null."}}},"NamedRef":{"type":"object","required":["_id","name"],"properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"}}},"Grape":{"type":"object","required":["_id","name"],"properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"},"displayName":{"type":"string","description":"The name as this wine's label region uses it."}}},"Bottle":{"type":"object","description":"One bottle. Fields that are not set may be absent.","required":["_id","cellar","status"],"properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"cellar":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"wineDefinition":{"oneOf":[{"type":"null"},{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},{"$ref":"#/components/schemas/WineSummary"}],"description":"The wine: an object in lists and the bottle detail, its id alone in the consume answer; null while the bottle waits on a wine request."},"pendingWineRequest":{"description":"The wine request the bottle waits on, when it has no wine yet.","oneOf":[{"type":"null"},{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"wineName":{"type":"string"},"producer":{"type":["string","null"]}}}]},"vintage":{"type":"string","examples":["2015","NV"]},"bottleSize":{"type":"string","examples":["750ml"]},"price":{"type":["number","null"]},"currency":{"type":["string","null"]},"rating":{"type":["number","null"]},"ratingScale":{"type":["string","null"],"enum":["5","20","100",null],"description":"\"5\" = stars, \"20\" = points out of 20, \"100\" = points out of 100."},"status":{"type":"string","enum":["active","ordered","drank","gifted","sold","other"],"description":"`active` = in the cellar, `ordered` = bought but not delivered yet; the rest say how it left."},"location":{"type":["string","null"],"description":"Free-text location."},"notes":{"type":["string","null"]},"purchaseDate":{"type":["string","null"],"format":"date-time"},"purchaseLocation":{"type":["string","null"]},"drinkFrom":{"type":["integer","null"],"minimum":1900,"maximum":2200},"peakFrom":{"type":["integer","null"],"minimum":1900,"maximum":2200},"peakUntil":{"type":["integer","null"],"minimum":1900,"maximum":2200},"drinkTo":{"type":["integer","null"],"minimum":1900,"maximum":2200},"openedAt":{"type":["string","null"],"format":"date-time"},"consumedAt":{"type":["string","null"],"format":"date-time"},"consumedReason":{"type":["string","null"],"enum":["drank","gifted","sold","other",null]},"addedToCellarAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"defaultImageUrl":{"type":["string","null"]},"vintageImageUrl":{"type":["string","null"]},"pendingImageUrl":{"type":["string","null"]},"otherVintageImageUrl":{"type":["string","null"]}}},"BottleList":{"type":"object","required":["bottles"],"properties":{"bottles":{"type":"object","required":["count","total","limit","skip","items"],"properties":{"count":{"type":"integer","minimum":0,"description":"Items on this page."},"total":{"type":"integer","minimum":0,"description":"Items across all pages."},"limit":{"type":"integer","minimum":1,"maximum":200},"skip":{"type":"integer","minimum":0,"description":"The offset of this page."},"items":{"type":"array","items":{"$ref":"#/components/schemas/Bottle"}}}}}},"BottleDetail":{"type":"object","required":["bottle","userRole","rackInfo"],"properties":{"bottle":{"$ref":"#/components/schemas/Bottle"},"userRole":{"type":"string","enum":["owner","editor","viewer"],"description":"The caller's role in the bottle's cellar."},"rackInfo":{"description":"Where the bottle sits, or null when it is in no rack.","oneOf":[{"type":"null"},{"type":"object","required":["rackId","rackName","position"],"properties":{"rackId":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"rackName":{"type":"string"},"position":{"type":"integer","minimum":1}}}]},"defaultImageUrl":{"type":["string","null"]},"vintageImageUrl":{"type":["string","null"]},"pendingImageUrl":{"type":["string","null"]},"otherVintageImageUrl":{"type":["string","null"]}}},"RackList":{"type":"object","required":["racks"],"properties":{"racks":{"type":"array","items":{"$ref":"#/components/schemas/RackSummary"}}}},"RackSummary":{"type":"object","required":["_id","cellar","name","type","rows","cols","slots"],"properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"cellar":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"},"group":{"type":["string","null"],"description":"A user-named group, such as a room or an appliance."},"type":{"type":"string","enum":["grid","x-rack","hex","triangle","stack","cube","shelf","cabinet"]},"rows":{"type":"integer","minimum":1,"description":"Cabinets: the number of shelves."},"cols":{"type":"integer","minimum":1},"typeConfig":{"type":"object","description":"Type-specific geometry (shelf rows, two-deep, …). Read `GET /api/inventory` for shelf/row/col instead of computing them."},"isModular":{"type":"boolean"},"slots":{"type":"array","items":{"type":"object","required":["position","bottle"],"properties":{"position":{"type":"integer","minimum":1},"bottle":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]}}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"StatsOverview":{"type":"object","required":["stats"],"properties":{"stats":{"type":"object","description":"Statistics over the cellars the caller OWNS. Only the parts listed here are part of the contract; the answer carries more, which may change.","required":["overview","maturity","pace","byType","byCountry","cellarBreakdown","topProducers","urgencyLadder"],"properties":{"overview":{"type":"object","required":["totalBottles","totalConsumed","uniqueWines","totalCellars","totalCountries","totalGrapes","totalValue","currency","avgPrice","avgRating","targetRatingScale","oldestVintage","newestVintage","bottlesDrunk","bottlesGifted","bottlesSold"],"properties":{"totalBottles":{"type":"integer","minimum":0},"totalConsumed":{"type":"integer","minimum":0},"uniqueWines":{"type":"integer","minimum":0},"totalCellars":{"type":"integer","minimum":0},"totalCountries":{"type":"integer","minimum":0},"totalGrapes":{"type":"integer","minimum":0},"totalValue":{"type":"number","description":"In `currency`, from the bottles' purchase prices."},"currency":{"type":"string","description":"The caller's preferred currency."},"avgPrice":{"type":"number"},"avgRating":{"type":["number","null"],"minimum":0,"maximum":100,"description":"The average of the bottles' own ratings on a 0–100 scale, whatever scale each was given in. For display in `targetRatingScale`: × 5 / 100 for stars, × 20 / 100 for points out of 20."},"targetRatingScale":{"type":"string","enum":["5","20","100"],"description":"\"5\" = stars, \"20\" = points out of 20, \"100\" = points out of 100."},"oldestVintage":{"type":["integer","null"]},"newestVintage":{"type":["integer","null"]},"bottlesDrunk":{"type":"integer","minimum":0},"bottlesGifted":{"type":"integer","minimum":0},"bottlesSold":{"type":"integer","minimum":0}}},"maturity":{"type":"object","description":"Bottles per drink-window stage.","required":["declining","late","peak","early","notReady","noProfile"],"properties":{"declining":{"type":"integer","minimum":0},"late":{"type":"integer","minimum":0},"peak":{"type":"integer","minimum":0},"early":{"type":"integer","minimum":0},"notReady":{"type":"integer","minimum":0},"noProfile":{"type":"integer","minimum":0}}},"pace":{"type":"object","required":["avgIntakePerYear","avgOutputPerYear","netPerYear","runway"],"properties":{"avgIntakePerYear":{"type":"number"},"avgOutputPerYear":{"type":"number"},"netPerYear":{"type":"number"},"runway":{"type":["integer","null"],"description":"Years until the cellar is empty at the current pace, or null."}}},"byType":{"type":"object","additionalProperties":{"type":"integer","minimum":0},"description":"Bottle count per wine type; `unknown` for bottles without one."},"byCountry":{"type":"array","items":{"type":"object","required":["name","count"],"properties":{"name":{"type":"string"},"count":{"type":"integer","minimum":0}}}},"cellarBreakdown":{"type":"array","items":{"type":"object","required":["id","name","bottleCount","value"],"properties":{"id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"},"bottleCount":{"type":"integer","minimum":0},"value":{"type":"number"}}}},"topProducers":{"type":"array","maxItems":10,"items":{"type":"object","required":["name","count"],"properties":{"name":{"type":"string"},"count":{"type":"integer","minimum":0}}}},"urgencyLadder":{"type":"array","maxItems":10,"description":"Bottles past their peak, most urgent first.","items":{"type":"object","required":["id","cellarId","name","producer","vintage","type","price","status"],"properties":{"id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"cellarId":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"},"producer":{"type":"string"},"vintage":{"type":"string"},"type":{"type":"string"},"price":{"type":["number","null"],"description":"In the overview's currency."},"status":{"type":"string","enum":["late","declining"]}}}}}}}},"NotificationList":{"type":"object","required":["notifications","unreadCount"],"properties":{"notifications":{"type":"array","maxItems":30,"items":{"$ref":"#/components/schemas/Notification"},"description":"The 30 newest, newest first."},"unreadCount":{"type":"integer","minimum":0,"description":"Unread across all notifications, not only these 30."}}},"Notification":{"type":"object","required":["_id","type","title","message","read","createdAt"],"properties":{"_id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"type":{"type":"string","description":"What it is about. New types appear over time; show the title and message of a type you don't know."},"title":{"type":"string"},"message":{"type":"string"},"link":{"type":["string","null"],"description":"A path in the web app, relative to the server."},"read":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"UnreadCount":{"type":"object","required":["unreadCount","newestId"],"properties":{"unreadCount":{"type":"integer","minimum":0},"newestId":{"type":["string","null"],"pattern":"^[a-f0-9]{24}$","description":"The newest notification's id: when it changes, fetch the list."}}},"ConsumeRequest":{"type":"object","properties":{"reason":{"type":"string","enum":["drank","gifted","sold","other"],"default":"drank"},"rating":{"type":"number","description":"Your rating of the bottle, in `consumedRatingScale`."},"consumedRatingScale":{"type":"string","enum":["5","20","100"],"description":"The scale of `rating`. Defaults to `\"5\"` (stars) when left out."},"note":{"type":"string","maxLength":1000},"consumedAt":{"type":"string","description":"When it was drunk, if not now: a date or date-time, not in the future."},"ifActive":{"type":"boolean","description":"Send `true`. The bottle is then consumed only if it is still in the cellar; one that already left (consumed elsewhere) answers 409 `state_changed` instead of being overwritten."}}},"ConsumeResponse":{"type":"object","required":["bottle"],"properties":{"bottle":{"$ref":"#/components/schemas/Bottle"}}},"ClimateIngestRequest":{"type":"object","required":["readings"],"properties":{"readings":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["channel","type","value"],"properties":{"channel":{"type":"string","pattern":"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$","description":"Your name for the sensor, e.g. `fridge`."},"type":{"type":"string","enum":["temperature","humidity"]},"value":{"type":"number","description":"°C, or % relative humidity."},"ts":{"type":"string","format":"date-time","description":"When it was measured, if not now."}}}},"firmware":{"type":"string","maxLength":100},"rssi":{"type":"number"}}},"ClimateIngestResponse":{"type":"object","required":["accepted","rejected","intervalS"],"properties":{"accepted":{"type":"integer","minimum":0},"rejected":{"type":"integer","minimum":0},"errors":{"type":"array","maxItems":10,"items":{"type":"object","required":["index","reason"],"properties":{"index":{"type":"integer","minimum":0},"reason":{"type":"string"}}}},"intervalS":{"type":"integer","minimum":1,"description":"Seconds to wait before the next post."}}},"TokenRevoked":{"type":"object","required":["message","id","name"],"properties":{"message":{"type":"string"},"id":{"type":"string","pattern":"^[a-f0-9]{24}$","examples":["66f0c0ffee0123456789abcd"]},"name":{"type":"string"}}}}}}