API Reference¶
Auto-generated
This page will be automatically generated from Google-style docstrings via mkdocstrings.
For interactive API docs with a running backend:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
Print & Export (REQ-032)¶
All print endpoints are located under the tenant-scoped path /api/v1/t/{slug}/print/ and require a valid JWT token. Access rights mirror the permissions of the underlying data (REQ-024 RBAC) — anyone who may read a nutrient plan may also print it.
Common query parameters (all endpoints):
| Parameter | Type | Default | Values |
|---|---|---|---|
locale | string | de | de, en |
format | string | pdf | pdf, csv (tabular templates only) |
Nutrient Plan PDF¶
Exports a complete nutrient plan as a PDF including the phase table, mixing instructions, water configuration, and CalMag / flushing notes.
Path parameters:
| Parameter | Description |
|---|---|
slug | Tenant slug |
plan_key | ArangoDB key of the NutrientPlan document |
Response: application/pdf with Content-Disposition: attachment; filename="nutrient-plan-{plan_key}.pdf"
Example:
curl -X GET \
"https://api.example.com/api/v1/t/my-garden/print/nutrient-plan/nutrient_plans/42?locale=en" \
-H "Authorization: Bearer <token>" \
--output nutrient-plan.pdf
Care Checklist PDF¶
Exports all due care tasks for a given date as a checklist with tick boxes, grouped by urgency (overdue, due today, coming up).
Query parameters (in addition to locale and format):
| Parameter | Type | Default | Description |
|---|---|---|---|
date | string (ISO 8601) | Today's date | Reference date for due tasks, e.g. 2026-04-01 |
Response: application/pdf with Content-Disposition: attachment; filename="care-checklist-{date}.pdf"
Example:
curl -X GET \
"https://api.example.com/api/v1/t/my-garden/print/care-checklist?date=2026-04-15&locale=en" \
-H "Authorization: Bearer <token>" \
--output care-checklist.pdf
Plant Info Cards / Label PDF¶
Prints compact info cards with a QR code for one or more plant instances. The QR code contains the deep-link URL to the respective plant in the app.
Query parameters (in addition to locale):
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
plant_keys | string | Yes | — | Comma-separated ArangoDB keys of the plant instances (at least 1) |
fields | string | No | name,scientific_name,planted_date | Comma-separated fields to show on the card |
layout | string | No | grid_2x4 | single (A6), grid_2x4 (8 per A4), grid_3x3 (9 per A4) |
qr_size_mm | integer | No | 25 | QR code side length in mm (min: 20, max: 60) |
Possible values for fields:
name, scientific_name, family, planted_date, current_phase, location, cultivar, note
The QR code is always included and cannot be deselected via fields.
Response: application/pdf with Content-Disposition: attachment; filename="plant-labels.pdf"
Example — 8 cards per A4 page with plant name, scientific name and planting date:
curl -X GET \
"https://api.example.com/api/v1/t/my-garden/print/plant-labels\
?plant_keys=plant_instances/101,plant_instances/102,plant_instances/103\
&fields=name,scientific_name,planted_date,location\
&layout=grid_2x4\
&qr_size_mm=25\
&locale=en" \
-H "Authorization: Bearer <token>" \
--output labels.pdf
Error codes:
| HTTP status | Meaning |
|---|---|
400 | Invalid parameters (e.g. unknown layout value, qr_size_mm out of range) |
401 | Not authenticated |
403 | No permission for this tenant or resource |
404 | Plan key or plant instance key not found |
422 | Required parameter missing (e.g. plant_keys for /plant-labels) |
List Available Templates¶
Returns a list of all registered print templates.
This endpoint is not tenant-scoped and only requires a valid authentication token.
Example response:
[
{
"type": "nutrient_plan",
"label_de": "Nährstoffplan",
"label_en": "Nutrient Plan",
"formats": ["pdf"],
"locales": ["de", "en"]
},
{
"type": "care_checklist",
"label_de": "Pflege-Checkliste",
"label_en": "Care Checklist",
"formats": ["pdf"],
"locales": ["de", "en"]
},
{
"type": "plant_label",
"label_de": "Pflanzen-Infokarte",
"label_en": "Plant Info Card",
"formats": ["pdf"],
"locales": ["de", "en"]
}
]
Browser Push / PWA Notifications¶
All three endpoints are located under the tenant-scoped path /api/v1/t/{tenant_slug}/notifications/pwa/ and require a valid JWT token.
Retrieve the VAPID Public Key¶
Returns the instance's VAPID public key. The browser requires this key to create a push subscription.
Response (200):
If no VAPID key pair is configured, the endpoint responds with 503 Service Unavailable.
Register a Push Subscription¶
Registers the current device for browser push notifications. The subscription data is provided by the browser after calling PushManager.subscribe().
Request body:
{
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "...",
"auth": "..."
}
}
Response: 201 Created on success, 409 Conflict if the subscription for this device is already registered.
Deregister a Push Subscription¶
Removes the subscription for the current device. After this, no browser push notifications will be sent to that device.
Request body:
Response: 204 No Content on success, 404 Not Found if the subscription was not found.
See Also¶
- Print Views & Export — User Guide
- Fertilization Logic
- Care Reminders
- Environment Variables — Browser Push (VAPID)
Site Weather Forecast & Frost Early-Warning ¶
Both endpoints live under the tenant-specific path /api/v1/t/{tenant_slug}/ and require a valid JWT token. There is no separate role restriction — any active tenant member (including the Viewer role) may read. Both endpoints are graceful: if no weather source is configured, no GPS coordinates are stored for the site, or the weather forecast feature is disabled by the operator (WEATHER_ENABLED=false), they return empty/null forecast fields instead of an error.
Retrieve a Site's Daily Weather Forecast¶
Returns the in-horizon daily forecasts for a site (from the weather source infrastructure) plus the aggregated proactive frost early-warning summary. Backs the "Weather forecast" dashboard widget.
Response (200): SiteWeatherForecastResponse
{
"site_key": "sites/42",
"forecasts": [
{
"forecast_date": "2026-07-07",
"temp_min_c": -1.5,
"temp_max_c": 6.0,
"precipitation_mm": 0.0,
"wind_speed_kmh": 10.0,
"humidity_percent": 80.0,
"weather_code": "clear",
"source": "open-meteo",
"data_kind": "forecast"
}
],
"forecast_frost_warning": true,
"forecast_min_temperature": -1.5,
"forecast_expected_date": "2026-07-07",
"forecast_source": "open-meteo"
}
| Field | Type | Meaning |
|---|---|---|
forecasts | list | Daily forecasts within the configured forecast horizon (default: today + 1 day), each with a provenance label (source, data_kind) |
forecast_frost_warning | boolean | null | true when at least one day in the horizon reaches a minimum temperature at or below the forecast frost threshold; null when no usable forecast is available |
forecast_min_temperature | number | null | Minimum temperature of the earliest expected frost day |
forecast_expected_date | string | null | Date of the earliest expected frost day |
forecast_source | string | null | Weather source that this frost day comes from |
Additional Fields on a Location's Frost Warning (Location)¶
The existing reactive frost warning now additionally returns the proactive forecast for the site that this location belongs to. The reactive frost_warning field is unchanged, so the Home Assistant coordinator stays compatible.
Response (200): FrostWarningResponse — in addition to the existing fields (location_key, frost_warning, temperature_celsius, threshold_celsius, source, entity_id):
| Field | Type | Meaning |
|---|---|---|
forecast_frost_warning | boolean | null | Proactive forecast for the associated site (additive, see above) |
forecast_min_temperature | number | null | Expected minimum temperature of the earliest frost day |
forecast_expected_date | string | null | Date of the earliest expected frost day |
forecast_source | string | null | Provenance of the underlying forecast |
See Also¶
- Dashboard: Weather Forecast and Frost Early-Warning — User Guide
- Notifications: Frost Early-Warning — User Guide
- Weather Sources per Location — User Guide
- Environment Variables — Weather Forecast & Frost Early-Warning
Site Climate Normals (NASA POWER) ¶
Returns a site's long-term monthly climate normals for the "Climate at the Site" section of the site detail page. The endpoint lives under the tenant-specific path /api/v1/t/{tenant_slug}/ and requires a valid JWT token; any active tenant member (including the Viewer role) may read. Site ownership is verified server-side (404 unknown / 403 foreign). The endpoint is graceful: if no climate normals exist yet for an owned site (background fetch not yet run), it returns an empty normals list instead of an error.
Retrieve a Site's Climate Normals¶
Response (200): SiteClimateResponse
{
"site_key": "sites/42",
"normals": [
{
"source": "nasa-power",
"attribution": "Klima- und Strahlungsdaten: NASA POWER (power.larc.nasa.gov)",
"period_start_year": 1991,
"period_end_year": 2020,
"monthly_temp_min_c": [-3.1, -2.6, 0.4, 3.8, 8.2, 11.4, 13.1, 12.8, 9.6, 5.7, 1.3, -1.9],
"monthly_temp_max_c": [2.4, 3.9, 8.1, 13.2, 18.0, 21.3, 23.6, 23.2, 18.9, 13.1, 7.0, 3.2],
"monthly_temp_avg_c": [-0.4, 0.6, 4.2, 8.5, 13.1, 16.4, 18.4, 18.0, 14.2, 9.4, 4.1, 0.6],
"monthly_precip_mm": [42.0, 33.0, 40.0, 37.0, 55.0, 68.0, 62.0, 58.0, 45.0, 39.0, 48.0, 47.0],
"monthly_solar_mj_m2": [4.1, 7.2, 11.5, 16.3, 19.8, 21.0, 20.4, 17.6, 12.5, 7.4, 4.0, 3.1],
"coldest_month_min_c": -3.1,
"annual_temp_avg_c": 8.9,
"annual_precip_mm": 574.0,
"fetched_at": "2026-07-01T03:12:00Z"
}
]
}
| Field | Type | Meaning |
|---|---|---|
normals | list | One entry per contributing source; currently only nasa-power. Empty as long as the monthly background fetch hasn't run yet for this site. |
source | string | Provenance identifier of the entry (nasa-power) |
attribution | string | The source's license/attribution notice (mandatory CC-BY notice), meant for display directly next to the data |
period_start_year / period_end_year | number | null | Reference period of the climate normal (e.g. 1991–2020) |
monthly_temp_min_c / monthly_temp_max_c / monthly_temp_avg_c | list[12] | Monthly minimum, maximum, and average temperature, index 0 = January |
monthly_precip_mm | list[12] | Monthly precipitation in mm |
monthly_solar_mj_m2 | list[12] | Monthly solar radiation in MJ/m² |
coldest_month_min_c | number | null | Minimum of the coldest month — an input for the automatic hardiness-zone derivation |
annual_temp_avg_c / annual_precip_mm | number | null | Annual average / annual total |
fetched_at | datetime | Time this record was last fetched from the source |
API only: triggering climate normals manually
There is no dedicated endpoint to manually trigger the fetch for a single site. Population runs exclusively via the monthly Celery task app.tasks.climate_tasks.fetch_climate_normals (operator configuration, see Environment Variables — Climate Normals).
See Also¶
Hardiness Zones (USDA) ¶
Automatic derivation of a site's USDA hardiness zone from its climate normals (coldest monthly mean minimum temperature), following the license-free USDA zone schema (26 half-zones 1a–13b, no proprietary USDA/PHZM/PRISM map data). Replaces the free-text Site.climate_zone field for the traffic-light comparison, which is kept for compatibility and automatically synced. Feeds the hardiness traffic light for perennial plants.
The global catalog is reference data (like botanical families) and lives without a tenant prefix under /api/v1/hardiness-zones; per-site derivation and read access are tenant-scoped under /api/v1/t/{tenant_slug}/sites/{site_key}/. All endpoints require a valid JWT token.
Listing the Global Zone Catalog¶
Response (200): A list of HardinessZoneResponse, coldest zone first.
[
{
"zone": "7a",
"zone_number": 7,
"subzone": "a",
"temp_min_c": -17.7,
"temp_max_c": -15.0,
"temp_min_f": 0.0,
"temp_max_f": 5.0,
"description_de": "Mild-gemäßigtes Klima weiter Tieflandregionen. Günstige Zone für die Freilandkultur der meisten winterharten Stauden und Gehölze.",
"representative_regions_de": ["Norddeutsches Tiefland", "Wiener Becken", "Genferseeregion"],
"typical_last_frost_md": "05-08",
"typical_first_frost_md": "10-20"
}
]
Only the DACH-relevant zones 5a–9a carry curated German descriptions and example regions; all other zones across the worldwide 1a–13b spectrum have a generic description with no representative_regions_de.
Retrieving a Single Zone¶
Path parameter: zone — the zone label in <number><a|b> format, e.g. 7a.
Error codes: 404 when zone isn't a valid label in the catalog.
Reading a Site's Hardiness Zone¶
Any active tenant member (including the Viewer role) may read. Site ownership is verified server-side (404 for an unknown/foreign site).
Response (200): SiteHardinessResponse
{
"site_key": "sites/42",
"hardiness_zone": "7a",
"hardiness_zone_source": "derived_gps",
"hardiness_zone_resolved_at": "2026-07-01T05:00:00Z",
"mean_annual_minimum_c": -17.2,
"last_frost_date_avg": "2026-05-08",
"first_frost_date_avg": "2026-10-20",
"zone": { "zone": "7a", "...": "full catalog entry, as above" }
}
hardiness_zone_source ∈ manual (never overwritten automatically), derived_gps (derived from climate normals). The values derived_postal and frostline_us are reserved for future, not-yet-implemented derivation paths.
Re-Deriving a Site's Hardiness Zone¶
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
force | boolean | false | When true, discards an already manually set zone as well and re-derives it from the climate normals. |
Without force=true, a manually set zone (hardiness_zone_source: manual) is left untouched — the endpoint then returns the existing zone unchanged. It also pre-fills the site's frost reference dates (last_frost_date_avg, first_frost_date_avg) from the derived zone's catalog entry, when not already set. A regular PUT call on the site with hardiness_zone set in the request body instead marks the zone directly as manual.
Response (200): SiteHardinessResponse (see above).
Error codes:
| HTTP status | Meaning |
|---|---|
404 | Site not found or doesn't belong to the tenant |
422 | No climate normals with a usable minimum temperature exist yet for the site (VALIDATION_ERROR) — climate normals must be fetched for the site first |
API only: hardiness-zone operation
Neither a button to trigger an immediate re-derivation nor a display of the derived zone with its provenance are wired into the site form of the web interface yet. Independent of that, the automatic derivation already runs fully automatically in the background via a quarterly Celery task (see Environment Variables — Hardiness Zones); the endpoint documented here is for triggering an immediate manual re-derivation.
See Also¶
- Climate Zones & Hardiness — User Guide
- Overwintering — User Guide
- Site Climate Normals (NASA POWER)
- Environment Variables — Hardiness Zones
Plant Instances: Removal with Ending Type & Survival Statistics¶
All endpoints are located under the tenant-scoped path /api/v1/t/{tenant_slug}/plant-instances/ and require a valid JWT token.
Remove a Plant (with Optional Ending Classification)¶
Removes a plant instance. The request body is optional and backward compatible: an empty body (or no body at all) matches the previous plain removal without classification.
Request body (optional):
| Field | Type | Required | Values |
|---|---|---|---|
termination_type | string | null | No | harvested, senesced, died, cancelled |
termination_cause | string | null | No — only valid together with termination_type: "died" | disease, pest, frost, heat, drought, waterlogging, neglect, mechanical, unknown |
Behaviour:
- Without a body, or with
termination_type: null: plain removal, as before these fields were introduced —removed_onis set, no further classification. - With
termination_type: "died": the current growth phase is frozen via the phase-transition engine (the open phase-history entry is closed without triggering a senescence transition), andtermination_causeis recorded for the loss-cause analysis. - For any
termination_typevalue: open tasks and care reminders for the plant are removed from the queue; completed/skipped tasks remain as history.
Response (200): PlantResponse — now additionally includes the termination_type and termination_cause fields (both null when not classified).
Error codes:
| HTTP status | Meaning |
|---|---|
404 | Plant instance not found or does not belong to the tenant |
422 | termination_cause set but termination_type is not died (VALIDATION_ERROR) |
Example — loss due to pest infestation:
curl -X POST \
"https://api.example.com/api/v1/t/my-garden/plant-instances/plant_instances/101/remove" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"termination_type": "died", "termination_cause": "pest"}'
Get Survival Statistics¶
Returns a tenant-wide analysis of all plant instances: survival rate, breakdown by ending type, by growth phase (unplanned losses only), and by loss cause.
Response (200):
{
"total": 42,
"terminated": 18,
"active": 24,
"died": 3,
"survived": 39,
"survival_rate": 0.9286,
"by_termination_type": [
{ "termination_type": "harvested", "count": 12 },
{ "termination_type": "died", "count": 3 },
{ "termination_type": "cancelled", "count": 2 },
{ "termination_type": "senesced", "count": 1 }
],
"by_termination_cause": [
{ "termination_cause": "pest", "count": 2 },
{ "termination_cause": "frost", "count": 1 }
],
"loss_by_phase": [
{ "phase_name": "seedling", "count": 2 },
{ "phase_name": "vegetative", "count": 1 }
]
}
survived counts every plant that was not an unplanned loss — harvested, naturally senesced, cancelled and still-active plants all count as survived; only termination_type: "died" counts as a loss. loss_by_phase is aggregated by the resolved phase name (not the phase key), so the same canonical phase across different species is summed together, and sorted in descending order by count.
Route ordering
/survival-stats is declared before /{key} in the router so the literal path is not accidentally captured as a plant key.
See Also¶
- Growth Phases — User Guide: Removing a Plant
- Growth Phases — User Guide: Survival Rate and Loss-Cause Analysis
Plant Instances: Pup Ancestry (mother_key)¶
When a monocarpic mother plant automatically transitions into its final flowering phase, Kamerplanter automatically creates a new plant instance (the pup) and links it to the mother plant.
Additional Field in the Plant Instance Response¶
PlantResponse now additionally includes:
| Field | Type | Meaning |
|---|---|---|
mother_key | string | null | Key of the mother plant this instance descended from as a pup. null for directly created plants. |
The authoritative ancestry relationship is additionally stored as a descended_from graph edge (pup → mother); mother_key mirrors it for cheap frontend access without requiring a graph-traversal query.
Trigger and Behaviour¶
- The automatic pup spawn is triggered as soon as a plant species configured as monocarpic (
flowering_strategy: "monocarpic") automatically transitions into one of its terminal reproductive phases (flowering, fruiting, or ripening). - Exactly one new plant instance is created; re-evaluating the same transition does not create a second pup (idempotent — guarded by the existence of an inbound
descended_fromedge on the mother). - The pup inherits
tenant_key,species_key,cultivar_key, and the mother's location, but no slot (slot_key: null) — the mother plant keeps its slot while it senesces. Itsplanted_onis set to the transition date. - In addition to the edge, a
PropagationEventwithmethod: "clone"is persisted (mother → pup).
No dedicated endpoint, no manual trigger
The pup spawn is a side effect of the automatic phase transition (see Growth Phases — Automatic Phase Transitions) and has no dedicated REST endpoint for manual triggering or for querying propagation history. The full propagation API (ancestry traversal, listing propagation events per plant) remains REQ-017 follow-up work.
See Also¶
- Growth Phases — User Guide: Monocarpic Plants
- Propagation Management — User Guide
- Database Schema — Plant Instance Graph
- Error Handling
Lifecycle Configuration: Derived Field grown_as_annual¶
Botanically perennial species that are cultivated in practice like annuals (the classic example: the tomato) get a derived, read-only flag in the lifecycle response.
Additional Field in the Lifecycle Response¶
LifecycleResponse additionally includes:
| Field | Type | Meaning |
|---|---|---|
grown_as_annual | boolean | true when cultivation_cycle_type == "annual" while cycle_type != "annual" — i.e. the species is cultivated as an annual in practice even though it is not botanically annual. |
Derived, not persisted, not a request field
grown_as_annual is a server-computed response field (computed_field): it is re-derived from cycle_type and cultivation_cycle_type on every request, is never independently stored in the database, and cannot be set via POST/PUT — a value sent in the request body is ignored.
See Also¶
Season & Overwintering Automation¶
These endpoints read the automatically computed season state of a site and the automatically materialised overwintering profile of a plant. Both are derived without user interaction as soon as a plant is assigned to a frost-exposed location — by default, a location on an outdoor, greenhouse, or balcony site (OVERWINTERING_SITE_TYPES), or a location with a manual frost-exposure override on a site of another type (see Setting Frost Exposure for a Location) — on plant creation, on a site change, and additionally as a safety net from the daily season evaluation run — see Season Automation and Overwintering in the user guide.
All endpoints are under the tenant-scoped path /api/v1/t/{tenant_slug}/ and require a valid JWT token.
Read a Site's Season State¶
Response (200):
{
"site_key": "sites/12",
"season_state_id": "season-4f2a9c1b3d0e",
"phase": "pre_winter",
"trigger_tier": "live",
"trigger_reason_i18n_key": "pages.season.trigger.frostForecast",
"season_year": 2026,
"entered_phase_at": "2026-10-18T06:30:00Z",
"last_min_temp_c": 3.5,
"forecast_first_frost_date": "2026-10-24",
"estimated_first_frost_md": "10-20",
"estimated_last_frost_md": "04-15",
"evaluated_at": "2026-10-19T06:30:00Z"
}
phase ∈ growing, pre_winter, winter_dormancy, pre_spring. trigger_tier ∈ live, climatological, calendar — which cascade tier (see Season Automation) currently determines the state.
If no season state exists yet for the site, the endpoint evaluates it lazily and persists the result instead of returning 404.
Error Codes:
| HTTP Status | Meaning |
|---|---|
404 | Site not found or does not belong to the tenant |
409 | Site has no frost exposure: neither is its type outdoor, greenhouse, or balcony, nor is at least one of its locations manually marked frost-exposed — only frost-exposed sites run a season state |
Season Overview Across All Sites¶
Returns {"states": [ ... ]} with one SeasonStateResponse object (see above) per frost-exposed site of the tenant — that is, sites of type outdoor, greenhouse, or balcony, plus sites of other types with at least one location manually marked frost-exposed. Feeds the "Winter Protection" dashboard widget (see Personalizing the Dashboard).
Read a Plant's Overwintering Profile¶
Response (200): the OverwinteringProfile object, including auto_generated, user_overridden, derived_path (A = in-situ, B = relocated) and materialized_at.
Error Codes: 404 if the plant has no (materialised) profile — e.g. because it is winter-hardy, is not at a frost-exposed site, or has not yet transitioned into "winter approaching".
Read a Plant's Overwintering Status¶
Additive, read-only companion to GET .../overwintering: always returns 200, even without a profile at all — useful for the plant detail page to distinguish "winter-hardy", "protection needed, plan pending", and "site not frost-exposed" without abusing the profile endpoint's 404 case for that.
Response (200): PlantOverwinteringStatus object:
{
"has_profile": false,
"hardiness_light": "yellow",
"will_materialize": true,
"site_overwinterable": true
}
| Field | Meaning |
|---|---|
has_profile | Whether an overwintering profile is already materialised. |
hardiness_light | Winter-hardiness rating (green, yellow, red), or null if it cannot be determined (e.g. missing species or site assignment). |
will_materialize | Whether a profile is (still) auto-created — true only when site_overwinterable is true and the rating is not green. |
site_overwinterable | Whether the site type is frost-exposed at all (outdoor, greenhouse, balcony). false for indoor, windowsill, grow-tent, or an unresolvable site. |
Error Codes: none — the endpoint always responds with 200, even for a foreign or unresolvable plant (protects against a cross-tenant existence oracle via the 404 difference).
Override an Overwintering Profile¶
Sets individual fields of the profile and marks it user_overridden: true. From then on the automation only fills in missing fields, without overwriting values you've already set.
Error Codes:
| HTTP Status | Meaning |
|---|---|
404 | Plant or profile not found, or does not belong to the tenant |
422 | Invalid value, or the chosen protection measure contradicts the winter-hardiness rating (D5 invariant — e.g. "dig up & store" on a winter-hardy rating) |
Reset an Overwintering Profile to Automatic¶
Resets user_overridden to false and re-materialises the profile fully from the species profile and the site's winter-hardiness rating.
Error Codes: 404 if the plant or profile is not found or does not belong to the tenant.
See Also¶
- Season Automation — User Guide
- Overwintering — User Guide
- Environment Variables — Season & Overwintering Automation
- Error Handling
Plant Identification: Reference Image Contribution (Self-Hosted Recognition)¶
When creating a plant from a photo identification, a user can optionally contribute the identification photo as a training reference for the self-hosted DINOv2 recognition (see Assigning the Photo to the New Plant — User Guide).
Requires a valid JWT token and at least the tenant role grower. Only available when self-hosted DINOv2 recognition is active (INFERENCE_SERVICE_ENABLED=true) — the external Pl@ntNet path has no local reference index.
Request Body: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
image | file | Yes | JPEG or PNG image, maximum IDENTIFICATION_MAX_IMAGE_SIZE_MB |
species_key | string | Yes | Resolved species key the reference image is attached to |
No scientific_name field
The endpoint does not expect a scientific_name form field. The scientific name is derived server-side from the species_key record; any value sent alongside it is ignored.
Response (202 Accepted): ReferenceContributionResponse
| Field | Type | Meaning |
|---|---|---|
accepted | boolean | Whether the contribution was accepted and indexed |
pending_review | boolean | true while the contribution is quarantined (is_active=false) and does not yet affect other users' active recognition. Becomes false only after a platform admin approves it. |
species_key | string | The species key the reference image was attached to |
dim | integer | null | Dimensionality of the computed embedding vector |
Error Codes:
| HTTP Status | Meaning |
|---|---|
403 | Active tenant role below grower (e.g. viewer) |
404 | species_key does not reference a known species |
409 | Self-hosted recognition is not enabled (INFERENCE_SERVICE_ENABLED=false) |
413 | Image exceeds IDENTIFICATION_MAX_IMAGE_SIZE_MB |
415 | Content-Type is neither image/jpeg nor image/png |
422 | Image cannot be decoded (corrupt or not a valid image format) |
429 | Daily contribution quota (REFERENCE_CONTRIBUTION_RATE_LIMIT_PER_USER_DAY) exhausted |
Security model (quarantine, provenance, dedup)
Every contribution is stored with source="user_contributed", is_active=false, and the contributing user and tenant as provenance — it therefore does not affect other tenants' recognition until a platform admin has reviewed it. Re-submitting the same photo (SHA-256 hash of the normalized image) updates the existing row instead of creating another one. The original image itself is never persisted — only the embedding.
See Also¶
- Identify a Plant by Photo — User Guide: Assigning the Photo to the New Plant
- Curating Reference Images — User Guide
- Environment Variables — Photo Identification
- Error Handling
AI Assistant ¶
Partially available
The endpoints documented here are implemented and active. Of the full specification scope (including background tip-card generation, a tenant-settings endpoint, and provider management via API), only the endpoints listed below have been implemented so far — see the notes in each section.
Every endpoint responds with 404 Not Found when the platform operator has disabled AI features instance-wide (AI_FEATURES_ENABLED=false) — the AI API then effectively doesn't exist. Details on the three-stage toggle: AI Assistant — User Guide.
Public knowledge question (Light Mode capable)¶
No login required, IP rate-limited (AI_PUBLIC_RATE_LIMIT_PER_MIN, default 10/minute). No tenant or user context is passed to the knowledge base.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
question | string | Yes | 3–2000 characters |
language | de | en | No | Default: de |
Response (200): AiResponseSchema
{
"answer_text": "VPD (vapor pressure deficit) describes the difference ...",
"sources": [
{ "source_key": "vpd-basics", "source_type": "guide", "title": "VPD basics", "score": 0.87, "language": "de" }
],
"language": "de",
"language_mismatch_warning": false,
"uses_tenant_data": false,
"uses_cloud_provider": false,
"confidence": "high",
"fallback_species": null,
"cultivar_hint": null,
"model_name": "gemma3:12b",
"provider_type": "ollama",
"kb_version": "ks-1.4.2-idx-20260420",
"generated_at": "2026-07-11T10:15:00Z"
}
confidence is one of high | medium | low | none (ADR-002 — drops when the question references a tenant-owned species/cultivar that isn't in the knowledge base).
Response (200): { "healthy": true }
Tenant-scoped endpoints¶
The following endpoints live under /api/v1/t/{tenant_slug}/ai/ and require a valid JWT token plus an active tenant membership. Role-based restrictions (viewer/grower/admin) are not yet implemented in this version — every active member may call every endpoint.
| Method | Path | Description |
|---|---|---|
GET | /ai/tips?context_type=&context_key=&language= | Tip cards for a context (cache-first) |
POST | /ai/tips/refresh?context_type=&context_key=&language= | Force-regenerate tip cards (cache miss) |
POST | /ai/tips/{tip_key}/dismiss | Dismiss a tip |
POST | /ai/tips/{tip_key}/acted-on | Mark a tip as acted on |
GET | /ai/daily-tip?language= | A single tip of the day (may be null) |
POST | /ai/daily-tip/dismiss | Dismiss today's daily tip |
POST | /ai/explain | "Why?" explanation for a concrete item |
GET | /ai/conversations | List conversations |
POST | /ai/conversations | Start a new conversation |
POST | /ai/conversations/{conversation_key}/messages | Send a message — answer streams back as SSE |
DELETE | /ai/conversations/{conversation_key} | Delete a conversation immediately (GDPR Art. 17) |
GET | /ai/providers | List available providers (read-only) |
API only / operator configuration: enabling for a tenant
Every one of these endpoints additionally requires tenant.settings.ai_features_enabled=true — there is currently neither a UI nor a dedicated GET/PUT endpoint for this; the field can only be set directly on the tenant document. Without this, every tenant-scoped endpoint responds with 403 and { "detail": "ai.disabled_for_tenant" } (error code AI_DISABLED_FOR_TENANT).
POST /ai/explain expects the following request body:
| Field | Type | Description |
|---|---|---|
subject_type | task | reminder | phase_transition | feeding_event | Type of the item to explain |
subject_key | string | Key of the item |
question_template_id | string | ID of the curated question template |
language | de | en | Optional, default: de |
POST /ai/conversations/{conversation_key}/messages returns the answer as text/event-stream (SSE) with the event types token (a single answer token), done (the final AiResponseSchema as JSON), and error.
Error Codes (all tenant-scoped endpoints):
| HTTP Status | Error Code | Meaning |
|---|---|---|
404 | — | AI features disabled instance-wide (stage 1) |
403 | AI_DISABLED_FOR_TENANT | AI features disabled for this tenant (stage 2) |
403 | CONSENT_REQUIRED | Required consent missing (stage 3, consent_purpose in the body: ai_tenant_data_access or ai_cloud_processing) |
Global endpoints (platform admin)¶
Requires platform admin rights. Only mounted in full mode (KAMERPLANTER_MODE=full). Returns { "healthy": true|false }.
See Also¶
- AI Assistant — User Guide
- Privacy & GDPR — User Guide
- Environment Variables — AI Assistant
- Error Handling
CV Disease Diagnosis ¶
Photo-based condition diagnosis (disease, nutrient deficiency, and — as a secondary category — pest) from a leaf photo, distinct from species identification (Plant Identification) and from the dedicated Pest Detection: this diagnosis answers "what is wrong with the plant?", not "which species/pest is this?". Recognition runs self-hosted in the inference service; the uploaded photo is stripped of EXIF metadata server-side and is never persisted — only a SHA-256 fingerprint is kept (image_deleted_at is set on every response).
All endpoints live under the tenant-scoped path /api/v1/t/{tenant_slug}/cv-diagnosis/ and require a valid JWT token. Read endpoints (/status, /history) need no special tenant role; write endpoints (/diagnose, /diagnose/{request_key}/confirm) require at least the grower role.
Always a hypothesis — never an automatic treatment
Every response carries a never-empty disclaimer field. A CV diagnosis never automatically triggers a treatment and does not bypass any pre-harvest interval gate (see Integrated Pest Management (IPM)) — POST .../confirm creates at most an IPM inspection suggestion that you review and confirm yourself.
Check Availability¶
Response (200): CvDiagnosisStatusResponse
{
"available": false,
"feature_enabled": false,
"adapter_key": "local_cv_diagnosis",
"phenotype_available": false,
"class_count": 0
}
| Field | Type | Meaning |
|---|---|---|
available | boolean | Whether the feature can be used (feature_enabled and a loaded classifier model). Drives whether a future photo-diagnosis button is shown in the frontend. |
feature_enabled | boolean | Operator switch (CV_DIAGNOSIS_ENABLED), independent of whether a model is already loaded. |
adapter_key | string | Identifier of the active adapter. Currently only local_cv_diagnosis (self-hosted, no image data leaves the instance). |
phenotype_available | boolean | Whether the PlantCV phenotype pipeline is available in the inference service. |
class_count | integer | Number of disease/deficiency/pest classes supported by the loaded classifier. |
Run a Photo Diagnosis¶
Requires at least the tenant role grower — the viewer role receives 403. Consent plant_diagnosis is required in Full mode and is enforced server-side (403 CONSENT_REQUIRED without a granted consent); in Light mode the server-side check is skipped, since no consent subsystem exists there (see Privacy & GDPR).
Request Body: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
image | file | Yes | JPEG or PNG image, maximum CV_DIAGNOSIS_MAX_IMAGE_SIZE_MB (default 5 MB) |
plant_key | string | No | Plant instance the diagnosis is attached to |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
phenotype | boolean | false | Also compute PlantCV phenotype metrics (leaf area, green index, discolored/necrotic area ratio) — only effective when phenotype_available == true |
Response (200): CvDiagnosisResponse
{
"key": "plant_diagnosis_requests/abc123",
"plant_instance_key": "plant_instances/101",
"inspection_key": null,
"classifications": [
{
"label": "septoria_leaf_spot",
"category": "disease",
"scientific_name": null,
"probability": 0.74,
"highlight": false,
"matched_disease_key": "diseases/septoria",
"matched_pest_key": null,
"matched_symptom_slug": null
}
],
"phenotype": null,
"model_meta": {
"model_name": "kamerplanter-leaf-disease-v1",
"training_base": "imagenet-dinov2-backbone",
"fine_tuned_on": ["plantdoc-ccby4"],
"onnx_checksum": "sha256:...",
"model_version": "20260601",
"class_count": 17
},
"adapter_key": "local_cv_diagnosis",
"is_confident": false,
"disclaimer": "Only a hypothesis of the image recognition — not a confirmed diagnosis. Please verify professionally before treating; get a second opinion if unsure.",
"confirmed_labels": [],
"image_hash": "sha256:9f86d0...",
"image_deleted_at": "2026-07-11T14:30:02Z",
"created_at": "2026-07-11T14:30:00Z"
}
| Field | Meaning |
|---|---|
classifications[].category | disease, deficiency, pest, or healthy (no abnormality detected) |
classifications[].probability | Confidence 0.0–1.0. Hits below the display floor (CV_CLASSIFIER_CONFIDENCE_SHOW) are dropped and never appear in the list. |
classifications[].highlight | true at or above the highlight threshold (CV_CLASSIFIER_CONFIDENCE_HIGHLIGHT) — a UI emphasis hint only, never an auto-accept |
classifications[].matched_disease_key / matched_pest_key | Key matched against the IPM stammdaten (Integrated Pest Management), only set for category disease or pest |
classifications[].matched_symptom_slug | Only set for category == "deficiency" — REQ-010 has no dedicated deficiency stammdaten collection (yet), so matching runs via symptom slugs instead |
is_confident | true when at least one actionable hit (disease/deficiency/pest) is highlighted. Does not mean "confirmed" — it's a UI classification only |
model_meta | Model card / provenance: fine_tuned_on lists the training source (plantdoc-ccby4 — CC BY 4.0; PlantVillage is not used, see License Notices) |
image_hash / image_deleted_at | Evidence that no original image is stored — only the fingerprint is retained |
Error Codes:
| HTTP Status | Meaning |
|---|---|
403 | Active tenant role below grower, or consent plant_diagnosis missing (Full mode) |
413 | Image exceeds CV_DIAGNOSIS_MAX_IMAGE_SIZE_MB or the internal decompression-bomb pixel limit |
415 | Content-Type is neither image/jpeg nor image/png |
422 | Image cannot be decoded (corrupt or not a valid image format) |
503 | The self-hosted classifier is not enabled or not reachable (CV_DIAGNOSIS_ENABLED=false or no loaded model) |
Confirm a Diagnosis into an IPM Inspection Suggestion¶
Requires at least the tenant role grower. Creates an IPM inspection as a suggestion from the confirmed classes — never an automatic treatment; the pre-harvest interval gate always stays active.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
plant_key | string | Yes | Plant instance the created inspection is attached to |
confirmed_labels | list[string] | No | Class labels to confirm; defaults to the highlighted (highlight == true) classes when omitted |
Response (201): ConfirmDiagnosisResponse
{
"inspection_key": "inspections/42",
"detected_disease_keys": ["diseases/septoria"],
"detected_pest_keys": [],
"confirmed_labels": ["septoria_leaf_spot"]
}
Error Codes:
| HTTP Status | Meaning |
|---|---|
403 | Active tenant role below grower |
404 | request_key unknown or does not belong to the tenant (cross-tenant access fails indistinguishably — no existence oracle) |
Retrieve Diagnosis History¶
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Maximum number of entries (1–100) |
Response (200): List of CvDiagnosisResponse (see above), sorted by creation date descending, scoped to the signed-in user's own diagnoses within the current tenant.
License Notices¶
The classifier is fine-tuned on the PlantDoc dataset (CC BY 4.0, attribution required) plus curated own-data field images; the phenotype pipeline uses PlantCV (MPL-2.0, used unmodified as a library). PlantVillage is not used (license unclear). Full attribution text: NOTICE.md.
See Also¶
- My Plant Looks Sick — Symptom Diagnosis
- Pest Detection by Photo
- Integrated Pest Management (IPM)
- Privacy & GDPR — AI Disease Diagnosis
- Environment Variables — CV Disease Diagnosis
Aquaponics ¶
Aquaponics introduces fish-plant closed-loop systems: fish stock, water tests with automatically calculated free ammonia, biofilter cycling detection, feeding, and nutrient supplementation. The frontend currently covers only part of the API (creating/listing systems, recording a water test, reading cycling progress and water quality) — see Aquaponics — User Guide: For Technical Users / Self-Hosters for the full, still UI-less remainder of the API.
Tenant-scoped under /api/v1/t/{tenant_slug}/aquaponics/ (28 endpoints, write calls require at least the grower role, deleting a system requires admin):
| Resource Group | Endpoints (Selection) |
|---|---|
| Systems | GET/POST /systems, GET/PATCH/DELETE /systems/{key}, POST /systems/{key}/cycling-status |
| Fish stock | GET/POST /systems/{key}/fish-stocks, PATCH/DELETE /systems/{key}/fish-stocks/{stock_key}, POST .../mortality, GET .../biomass-history, GET .../mortality-rate |
| Water tests & nitrogen cycle | GET/POST /systems/{key}/water-tests, GET /systems/{key}/water-quality-status, GET /systems/{key}/nitrogen-cycle-chart, GET /systems/{key}/cycling-progress |
| Feeding | GET/POST /systems/{key}/feeding-events, GET /systems/{key}/feeding-recommendation, GET /systems/{key}/fcr-analysis |
| Supplementation & deficiencies | GET/POST /systems/{key}/supplementation, GET /systems/{key}/deficiency-check |
| Safety & health | GET /systems/{key}/safety-status, GET /systems/{key}/alerts, GET /systems/{key}/fish-health |
Global (not tenant-scoped, no write access needed) under /api/v1/fish-species/:
| Endpoint | Description |
|---|---|
GET /fish-species | All 8 seed fish species with temperature zones and species-specific limits |
GET /fish-species/by-temperature-zone/{zone} | Fish species filtered by temperature zone (coldwater, temperate, warmwater) |
GET /fish-species/{species_key} | A single fish species |
GET /fish-species/{species_key}/compatible-plants | Fish-plant compatibility via graph edges (temperature and nutrient match) |
See Also¶
Post-Harvest¶
All endpoints live under the tenant-scoped path /api/v1/t/{tenant_slug}/post-harvest/ and require a valid JWT token. Read endpoints accept any active membership; write endpoints require at least the grower role; deleting a batch is admin-only.
| Method & Path | Description | Minimum role |
|---|---|---|
GET /post-harvest | List the tenant's batches (optionally filtered by harvest_batch) | any membership |
POST /post-harvest/start-drying | Take a harvest batch into post-harvest processing (stage "drying") | grower |
GET /post-harvest/{key} | Batch details incl. the latest drying measurement and open mold-alert count | any membership |
POST /post-harvest/{key}/advance | Advance the batch to the next stage (forward, one step) | grower |
POST /post-harvest/{key}/drying-progress | Record a weight measurement (optionally also water activity, CO₂, snap-test result) | grower |
GET /post-harvest/{key}/drying-progress | List all drying measurements of the batch | any membership |
POST /post-harvest/{key}/observations | Record an environmental observation (may auto-raise a mold alert) | grower |
GET /post-harvest/{key}/observations | List all environmental observations of the batch | any membership |
GET /post-harvest/{key}/mold-alerts | List the batch's mold alerts | any membership |
DELETE /post-harvest/{key} | Delete a batch | admin |
Stage state machine: drying → curing → stored → released — forward only, one step per call. The drying → curing transition additionally requires dryness_progress_percent >= 95.
Error codes:
| HTTP status | Meaning |
|---|---|
403 | Active tenant role below the required minimum role |
404 | Batch not found or does not belong to the tenant |
422 | Invalid stage transition (backward, skip, or drying progress < 95% on drying → curing), or current_weight_g exceeds the batch's start weight |
Example — Start drying¶
curl -X POST \
"https://api.example.com/api/v1/t/mein-garten/post-harvest/start-drying" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"harvest_batch_key": "harvest_batches/42",
"species_type": "flower",
"drying_method": "hang_dry",
"target_moisture_percent": 10
}'
See Also¶
Environment Control & Actuators ¶
All endpoints live under the tenant-scoped path /api/v1/t/{tenant_slug}/ and require a valid JWT token. Read calls accept any active membership; write calls (create, command, override, rules, schedules, emergency stop) require at least the grower role; deleting an actuator requires admin. The UI currently covers only part of the API (creating/listing/deleting an actuator, direct on/off command, emergency stop with the fire_alarm scenario) — see Environment Control & Actuators — User Guide: For Technical Users / Self-Hosters for the full, still UI-less remainder of the API (schedules, rules, phase-linked profiles, envelope configuration).
| Resource group | Endpoints (selection) |
|---|---|
| Actuators | GET/POST /locations/{location_key}/actuators, GET /actuators, GET/PUT/DELETE /actuators/{key} |
| Command & override | POST /actuators/{key}/command, POST/DELETE /actuators/{key}/override, GET /actuators/{key}/state |
| Schedules | GET/POST /actuators/{key}/schedules, PUT/DELETE /actuators/{key}/schedules/{schedule_key}, POST .../toggle |
| Rules | GET/POST /actuators/{key}/rules, GET /rules, PUT/DELETE /actuators/{key}/rules/{rule_key}, POST .../toggle, POST /rules/{rule_key}/test |
| Control log | GET /actuators/{key}/events, GET /actuators/{key}/events/stats, GET /locations/{location_key}/control-events, GET /locations/{location_key}/control-status, GET /locations/{location_key}/energy |
| Phase-linked profiles | GET/POST /phase-control-profiles, GET/PUT/DELETE /phase-control-profiles/{key}, POST .../apply |
| Emergency stop | POST /emergency-stop |
Safety guarantees¶
Value envelope: every command, rule hit, schedule hit and override passes through the same backend chokepoint. A numeric value for an actuator without a configured min_value/max_value is refused (422 for a direct command or override; the affected actuator is skipped and logged for the automatic control loop). If an envelope is configured, every value is automatically clamped into [min_value, max_value] — non-finite values (NaN/Infinity) are likewise never passed through unchanged.
Time-limited override: POST /actuators/{key}/override requires expires_at as a mandatory field. An expires_at that already lies in the past is rejected with 422 — there is no implicit default duration.
POST /api/v1/t/my-garden/actuators/act_42/override
{
"expires_at": "2026-07-11T10:00:00Z",
"override_state": "on",
"reason": "Manual ventilation before the weekend"
}
Response (422) when expires_at has already passed:
{
"error_id": "err_...",
"error_code": "VALIDATION_ERROR",
"message": "Manual override expires_at must be in the future.",
"details": [],
"timestamp": "2026-07-11T09:00:00.000000+00:00",
"path": "/api/v1/t/my-garden/actuators/act_42/override",
"method": "POST"
}
Emergency stop — per-actuator fault tolerance: POST /emergency-stop handles every affected actuator in isolation. If switching a single actuator fails (e.g. Home Assistant unreachable), the call is not aborted — the response lists successfully switched (stopped, forced_on) and failed (failed) actuator keys separately:
See Also¶
- Environment Control & Actuators — User Guide
- Sensors — User Guide
- Environment Variables — Environment Control & Actuators
- Error Handling
Phase Definitions: Plants & Species per Phase ¶
Two thin, read-only endpoints feed the two additional lists on the phase-definition detail page: which of your own plants are currently in a phase, and which species traverse that phase in the global catalog.
List Species for a Phase Definition (Global)¶
Returns all species from the global catalog whose phase sequence includes the given phase definition. No tenant prefix — reference data like botanical families or the hardiness-zone catalog.
Requires a valid JWT token; no separate role restriction.
Path parameter: key — the phase definition's key.
Response (200): a list of PhaseDefinitionSpeciesResponse. Empty if no species traverses this phase (no 404).
[
{
"key": "species/123",
"scientific_name": "Solanum lycopersicum",
"common_names": ["Tomate", "Tomato"],
"typical_duration_days": 30,
"illustration": "phases/flowering.svg"
}
]
| Field | Type | Meaning |
|---|---|---|
key | string | The species' key |
scientific_name | string | Scientific (binomial) name |
common_names | list[string] | Common names |
typical_duration_days | integer | This phase's typical duration for the species — the species-specific override (override_duration_days) from the phase-sequence entry when set, otherwise the phase definition's default |
illustration | string | The phase definition's own illustration path (no per-species illustration yet) |
If a species is linked to the same phase definition through more than one phase sequence, it appears only once (de-duplicated by species key); a species-specific override, where set, takes precedence over the definition's default.
List the Tenant's Active Plant Instances in a Phase Definition¶
Returns the calling tenant's active plant instances whose current phase resolves to the given phase definition. Tenant-scoped (SEC-001) — an empty tenant context is rejected server-side rather than returning another tenant's data.
Requires a valid JWT token and an active tenant membership; any role may read.
Response (200): PlantInstancesInPhaseResponse. items is empty if none of the tenant's active plants are in this phase (no 404).
{
"total": 2,
"items": [
{
"key": "plant_instances/101",
"instance_id": "T3-01",
"plant_name": "Balcony Tomato",
"species_key": "species/123",
"species_scientific_name": "Solanum lycopersicum",
"species_common_names": ["Tomato"],
"location_key": "locations/5",
"location_name": "South Balcony",
"slot_key": "slots/12",
"slot_label": "Row 2, Pot 3",
"current_phase_key": "phase_sequence_entries/456",
"current_phase_started_at": "2026-06-20T08:00:00Z"
}
]
}
| Field | Type | Meaning |
|---|---|---|
total | integer | Total number of plant instances returned |
items[].current_phase_key | string | null | Raw value of PlantInstance.current_phase_key — usually a PhaseSequenceEntry key; for older records it may still be a legacy GrowthPhase key |
items[].current_phase_started_at | datetime | null | Timestamp of the last phase transition — the basis for the "days in phase" count computed client-side on the detail page |
"Active" means removed_on == null — removed plants (see Growth Phases — User Guide: Removing a Plant) do not appear here. A plant is matched to the phase definition via its current phase-sequence assignment (PhaseSequenceEntry.phase_definition_key); for legacy data without a phase sequence, a fallback via the shared canonical phase name of a legacy GrowthPhase additionally applies.
No days_in_phase field in the response
Days in phase are deliberately not returned by the backend; the UI derives them from current_phase_started_at instead, so the value stays current without a re-fetch.
Route order: /by-phase-definition/{phase_definition_key} is declared before /{key} in the router so the literal path is not accidentally captured as a plant key (the same pattern used for /survival-stats above).