openapi: "3.0.3" info: title: HerbAPI description: | Plant reference database for permaculture and agroforestry planning. Covers botanical families, species, cultivars, suppliers, companion planting relationships, and images. **Authentication:** Write operations require an authenticated session via OIDC (Authentik). Read endpoints are public. version: "1.0.0" contact: name: Sub-Net e.U. url: https://sub-net.at email: florian.berthold@sub-net.at servers: - url: / description: Current instance tags: - name: Health description: Liveness probe - name: Families description: Botanical families (e.g. Fabaceae, Rosaceae) - name: Species description: Plant species within a family - name: Cultivars description: Named cultivars / varieties of a species - name: Suppliers description: Seed and plant suppliers - name: Cultivar-Suppliers description: Link cultivars to suppliers with pricing info - name: Companions description: Companion planting relationships between species - name: Images description: Entity images stored in S3 - name: Search description: Full-text search across all entities - name: Auth description: OIDC authentication (Authentik) paths: # ── Health ────────────────────────────────────────────────────────── /health: get: tags: [Health] summary: Health check operationId: health responses: "200": description: Service is healthy content: application/json: schema: type: object properties: status: type: string example: ok # ── Families ──────────────────────────────────────────────────────── /api/v1/families: get: tags: [Families] summary: List families (paginated) operationId: listFamilies parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/search" responses: "200": description: Paginated list of families content: application/json: schema: $ref: "#/components/schemas/PaginatedFamilies" post: tags: [Families] summary: Create a family operationId: createFamily security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateFamily" responses: "200": description: Created family content: application/json: schema: $ref: "#/components/schemas/Family" "403": $ref: "#/components/responses/Forbidden" /api/v1/families/{ref}: parameters: - $ref: "#/components/parameters/ref" get: tags: [Families] summary: Get a family by slug or UUID operationId: getFamily responses: "200": description: Family details content: application/json: schema: $ref: "#/components/schemas/Family" "404": $ref: "#/components/responses/NotFound" put: tags: [Families] summary: Update a family operationId: updateFamily security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateFamily" responses: "200": description: Updated family content: application/json: schema: $ref: "#/components/schemas/Family" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" delete: tags: [Families] summary: Delete a family (admin only) operationId: deleteFamily security: - session: [] responses: "200": $ref: "#/components/responses/Deleted" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" # ── Species ───────────────────────────────────────────────────────── /api/v1/species: get: tags: [Species] summary: List species (paginated, filterable) operationId: listSpecies parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/search" - name: family in: query description: Filter by family slug schema: type: string - name: plant_layer in: query description: "Filter by plant layer (e.g. canopy, shrub, herb, ground_cover, climber, root)" schema: type: string - name: nitrogen_fixer in: query description: Filter nitrogen-fixing species schema: type: boolean - name: dynamic_accumulator in: query description: Filter dynamic accumulator species schema: type: boolean - name: drought_tolerance in: query description: Filter by drought tolerance level schema: type: string - name: native_status in: query description: Filter by native status schema: type: string - name: min_nectar in: query description: Minimum nectar value (0-4) schema: type: integer format: int16 - name: min_bees in: query description: Minimum wild bee count schema: type: integer format: int32 responses: "200": description: Paginated list of species content: application/json: schema: $ref: "#/components/schemas/PaginatedSpecies" post: tags: [Species] summary: Create a species operationId: createSpecies security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSpecies" responses: "200": description: Created species content: application/json: schema: $ref: "#/components/schemas/Species" "403": $ref: "#/components/responses/Forbidden" /api/v1/species/{ref}: parameters: - $ref: "#/components/parameters/ref" get: tags: [Species] summary: Get a species by slug or UUID operationId: getSpecies responses: "200": description: Species details content: application/json: schema: $ref: "#/components/schemas/Species" "404": $ref: "#/components/responses/NotFound" put: tags: [Species] summary: Update a species operationId: updateSpecies security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSpecies" responses: "200": description: Updated species content: application/json: schema: $ref: "#/components/schemas/Species" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" delete: tags: [Species] summary: Delete a species (admin only) operationId: deleteSpecies security: - session: [] responses: "200": $ref: "#/components/responses/Deleted" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /api/v1/species/{ref}/companions: parameters: - $ref: "#/components/parameters/ref" get: tags: [Companions] summary: List companion relationships for a species operationId: listCompanionsForSpecies responses: "200": description: List of companion relationships content: application/json: schema: type: array items: $ref: "#/components/schemas/CompanionRelationship" "404": $ref: "#/components/responses/NotFound" # ── Cultivars ─────────────────────────────────────────────────────── /api/v1/cultivars: get: tags: [Cultivars] summary: List cultivars (paginated) operationId: listCultivars parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/search" - name: species in: query description: Filter by species slug schema: type: string responses: "200": description: Paginated list of cultivars content: application/json: schema: $ref: "#/components/schemas/PaginatedCultivars" post: tags: [Cultivars] summary: Create a cultivar operationId: createCultivar security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCultivar" responses: "200": description: Created cultivar content: application/json: schema: $ref: "#/components/schemas/Cultivar" "403": $ref: "#/components/responses/Forbidden" /api/v1/cultivars/{ref}: parameters: - $ref: "#/components/parameters/ref" get: tags: [Cultivars] summary: Get a cultivar by slug or UUID operationId: getCultivar responses: "200": description: Cultivar details content: application/json: schema: $ref: "#/components/schemas/Cultivar" "404": $ref: "#/components/responses/NotFound" put: tags: [Cultivars] summary: Update a cultivar operationId: updateCultivar security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCultivar" responses: "200": description: Updated cultivar content: application/json: schema: $ref: "#/components/schemas/Cultivar" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" delete: tags: [Cultivars] summary: Delete a cultivar (admin only) operationId: deleteCultivar security: - session: [] responses: "200": $ref: "#/components/responses/Deleted" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /api/v1/cultivars/{ref}/suppliers: parameters: - $ref: "#/components/parameters/ref" get: tags: [Cultivar-Suppliers] summary: List suppliers linked to a cultivar operationId: listCultivarSuppliers responses: "200": description: List of cultivar-supplier links content: application/json: schema: type: array items: $ref: "#/components/schemas/CultivarSupplier" "404": $ref: "#/components/responses/NotFound" post: tags: [Cultivar-Suppliers] summary: Link a supplier to a cultivar operationId: linkCultivarSupplier security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCultivarSupplier" responses: "200": description: Created link content: application/json: schema: $ref: "#/components/schemas/CultivarSupplier" "403": $ref: "#/components/responses/Forbidden" /api/v1/cultivars/{cid}/suppliers/{sid}: parameters: - name: cid in: path required: true description: Cultivar UUID schema: type: string format: uuid - name: sid in: path required: true description: Supplier UUID schema: type: string format: uuid delete: tags: [Cultivar-Suppliers] summary: Unlink a supplier from a cultivar operationId: unlinkCultivarSupplier security: - session: [] responses: "200": $ref: "#/components/responses/Deleted" "403": $ref: "#/components/responses/Forbidden" # ── Suppliers ─────────────────────────────────────────────────────── /api/v1/suppliers: get: tags: [Suppliers] summary: List all suppliers operationId: listSuppliers responses: "200": description: List of suppliers content: application/json: schema: type: array items: $ref: "#/components/schemas/Supplier" post: tags: [Suppliers] summary: Create a supplier operationId: createSupplier security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSupplier" responses: "200": description: Created supplier content: application/json: schema: $ref: "#/components/schemas/Supplier" "403": $ref: "#/components/responses/Forbidden" /api/v1/suppliers/{ref}: parameters: - $ref: "#/components/parameters/ref" get: tags: [Suppliers] summary: Get a supplier by slug or UUID operationId: getSupplier responses: "200": description: Supplier details content: application/json: schema: $ref: "#/components/schemas/Supplier" "404": $ref: "#/components/responses/NotFound" put: tags: [Suppliers] summary: Update a supplier operationId: updateSupplier security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSupplier" responses: "200": description: Updated supplier content: application/json: schema: $ref: "#/components/schemas/Supplier" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" delete: tags: [Suppliers] summary: Delete a supplier (admin only) operationId: deleteSupplier security: - session: [] responses: "200": $ref: "#/components/responses/Deleted" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" # ── Companions ────────────────────────────────────────────────────── /api/v1/companions: get: tags: [Companions] summary: List all companion relationships (with species names) operationId: listCompanions responses: "200": description: List of all companion relationships with resolved species names content: application/json: schema: type: array items: $ref: "#/components/schemas/CompanionWithNames" post: tags: [Companions] summary: Create a companion relationship operationId: createCompanion security: - session: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCompanion" responses: "200": description: Created relationship content: application/json: schema: $ref: "#/components/schemas/CompanionRelationship" "403": $ref: "#/components/responses/Forbidden" /api/v1/companions/{id}: parameters: - name: id in: path required: true description: Companion relationship UUID schema: type: string format: uuid delete: tags: [Companions] summary: Delete a companion relationship operationId: deleteCompanion security: - session: [] responses: "200": $ref: "#/components/responses/Deleted" "403": $ref: "#/components/responses/Forbidden" # ── Images ────────────────────────────────────────────────────────── /api/v1/images/{entity_type}/{entity_id}: parameters: - name: entity_type in: path required: true description: "Entity type (family, species, cultivar)" schema: type: string enum: [family, species, cultivar] - name: entity_id in: path required: true description: Entity UUID schema: type: string format: uuid get: tags: [Images] summary: List images for an entity operationId: listImagesForEntity responses: "200": description: List of images content: application/json: schema: type: array items: $ref: "#/components/schemas/Image" /api/v1/images: post: tags: [Images] summary: Upload an image (multipart/form-data) operationId: uploadImage security: - session: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [entity_type, entity_id, file] properties: entity_type: type: string enum: [family, species, cultivar] entity_id: type: string format: uuid file: type: string format: binary caption: type: string source_url: type: string license: type: string is_primary: type: string enum: ["true", "false", "1", "0"] responses: "200": description: Uploaded image metadata content: application/json: schema: $ref: "#/components/schemas/Image" "403": $ref: "#/components/responses/Forbidden" /api/v1/images/{id}: parameters: - name: id in: path required: true description: Image UUID schema: type: string format: uuid delete: tags: [Images] summary: Delete an image (admin only) operationId: deleteImage security: - session: [] responses: "200": $ref: "#/components/responses/Deleted" "403": $ref: "#/components/responses/Forbidden" /img/{path}: parameters: - name: path in: path required: true description: S3 object key (e.g. species/uuid/image.jpg) schema: type: string get: tags: [Images] summary: Serve an image from S3 operationId: serveImage responses: "200": description: Image binary data content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary image/webp: schema: type: string format: binary "404": description: Image not found # ── Search ────────────────────────────────────────────────────────── /api/v1/search: get: tags: [Search] summary: Full-text search across families, species and cultivars operationId: search parameters: - name: q in: query required: true description: Search query (whitespace-separated terms, ANDed) schema: type: string - name: limit in: query description: Maximum results (default 20, max 100) schema: type: integer default: 20 maximum: 100 responses: "200": description: Search results ranked by relevance content: application/json: schema: type: array items: $ref: "#/components/schemas/SearchResult" # ── Auth ──────────────────────────────────────────────────────────── /auth/oidc/login: get: tags: [Auth] summary: Initiate OIDC login (redirects to Authentik) operationId: oidcLogin responses: "302": description: Redirect to Authentik authorization endpoint /auth/oidc/callback: get: tags: [Auth] summary: OIDC callback (handles code exchange) operationId: oidcCallback parameters: - name: code in: query required: true schema: type: string - name: state in: query required: true schema: type: string responses: "302": description: Redirect to / after successful login /auth/oidc/logout: get: tags: [Auth] summary: Logout (destroys session) operationId: oidcLogout responses: "302": description: Redirect to / /auth/me: get: tags: [Auth] summary: Get current user info operationId: me responses: "200": description: Current authenticated user content: application/json: schema: $ref: "#/components/schemas/MeResponse" "401": description: Not authenticated components: securitySchemes: session: type: apiKey in: cookie name: id description: Session cookie set after OIDC login parameters: page: name: page in: query description: Page number (1-based, default 1) schema: type: integer default: 1 minimum: 1 per_page: name: per_page in: query description: Items per page (default 25, max 100) schema: type: integer default: 25 maximum: 100 search: name: search in: query description: Full-text search query schema: type: string ref: name: ref in: path required: true description: Slug or UUID schema: type: string responses: NotFound: description: Entity not found content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" Deleted: description: Successfully deleted content: application/json: schema: type: object properties: deleted: type: boolean example: true schemas: Error: type: object properties: error: type: string required: [error] # ── Family ────────────────────────────────────────────────────── Family: type: object properties: id: type: string format: uuid slug: type: string name_scientific: type: string name_en: type: string nullable: true name_de: type: string nullable: true description: type: string nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time CreateFamily: type: object required: [name_scientific] properties: name_scientific: type: string name_en: type: string name_de: type: string description: type: string UpdateFamily: type: object properties: name_scientific: type: string name_en: type: string name_de: type: string description: type: string PaginatedFamilies: type: object properties: data: type: array items: $ref: "#/components/schemas/Family" total: type: integer page: type: integer per_page: type: integer # ── Species ───────────────────────────────────────────────────── Species: type: object description: Full species record with all ecological and agronomic fields properties: id: type: string format: uuid slug: type: string family_id: type: string format: uuid name_scientific: type: string name_en: type: string nullable: true name_de: type: string nullable: true description: type: string nullable: true description_de: type: string nullable: true description_en: type: string nullable: true soil_moisture: type: string nullable: true drainage_requirement: type: string nullable: true organic_matter_pct: type: number nullable: true nitrogen_ppm: type: integer nullable: true phosphorus_ppm: type: integer nullable: true potassium_ppm: type: integer nullable: true boron_ppm: type: number nullable: true calcium_ppm: type: integer nullable: true copper_ppm: type: number nullable: true iron_ppm: type: number nullable: true magnesium_ppm: type: integer nullable: true manganese_ppm: type: number nullable: true molybdenum_ppm: type: number nullable: true sulfur_ppm: type: integer nullable: true zinc_ppm: type: number nullable: true ph_min: type: number nullable: true ph_max: type: number nullable: true soil_texture_preference: type: array items: type: string nullable: true hardiness_zone_usda: type: string nullable: true hardiness_zone_at: type: string nullable: true min_temp: type: number nullable: true max_temp: type: number nullable: true drought_tolerance: type: string nullable: true water_requirement_mm_week: type: number nullable: true waterlogging_tolerance: type: boolean nullable: true salt_tolerance: type: string nullable: true edibility_rating: type: integer nullable: true food_uses: type: string nullable: true food_uses_de: type: string nullable: true food_uses_en: type: string nullable: true medicinal_uses: type: string nullable: true medicinal_uses_de: type: string nullable: true medicinal_uses_en: type: string nullable: true other_uses: type: string nullable: true other_uses_de: type: string nullable: true other_uses_en: type: string nullable: true native_range: type: string nullable: true native_range_de: type: string nullable: true native_range_en: type: string nullable: true invasiveness: type: string nullable: true pollination_type: type: string nullable: true plant_layer: type: string nullable: true nitrogen_fixer: type: boolean nullable: true dynamic_accumulator: type: boolean nullable: true dynamic_accumulator_nutrients: type: array items: type: string nullable: true attracts_pollinators: type: boolean nullable: true attracts_beneficial_insects: type: boolean nullable: true wildlife_value: type: string nullable: true wildlife_value_de: type: string nullable: true wildlife_value_en: type: string nullable: true mulch_plant: type: boolean nullable: true ground_cover_quality: type: string nullable: true allelopathic: type: boolean nullable: true guild_role: type: array items: type: string nullable: true succession_stage: type: string nullable: true heavy_metal_tolerance: type: boolean nullable: true wikidata_qid: type: string nullable: true gbif_id: type: string nullable: true eppo_code: type: string nullable: true pfaf_url: type: string nullable: true nectar_value: type: integer nullable: true pollen_value: type: integer nullable: true wild_bee_count: type: integer nullable: true wild_bee_specialist_count: type: integer nullable: true butterfly_moth_count: type: integer nullable: true caterpillar_host_count: type: integer nullable: true caterpillar_specialist_count: type: integer nullable: true hoverfly_count: type: integer nullable: true beetle_count: type: integer nullable: true bird_count: type: integer nullable: true mammal_count: type: integer nullable: true native_status: type: string nullable: true naturadb_tags: type: string nullable: true primary_image_key: type: string nullable: true source_urls: type: array items: type: string nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time CreateSpecies: type: object required: [family_id, name_scientific] properties: family_id: type: string format: uuid name_scientific: type: string name_en: type: string name_de: type: string description: type: string description_de: type: string description_en: type: string plant_layer: type: string nitrogen_fixer: type: boolean dynamic_accumulator: type: boolean dynamic_accumulator_nutrients: type: array items: type: string drought_tolerance: type: string native_status: type: string nectar_value: type: integer pollen_value: type: integer ph_min: type: number ph_max: type: number soil_texture_preference: type: array items: type: string hardiness_zone_usda: type: string hardiness_zone_at: type: string min_temp: type: number max_temp: type: number salt_tolerance: type: string edibility_rating: type: integer food_uses: type: string food_uses_de: type: string food_uses_en: type: string medicinal_uses: type: string medicinal_uses_de: type: string medicinal_uses_en: type: string other_uses: type: string other_uses_de: type: string other_uses_en: type: string native_range: type: string native_range_de: type: string native_range_en: type: string invasiveness: type: string pollination_type: type: string attracts_pollinators: type: boolean attracts_beneficial_insects: type: boolean wildlife_value: type: string wildlife_value_de: type: string wildlife_value_en: type: string mulch_plant: type: boolean ground_cover_quality: type: string allelopathic: type: boolean guild_role: type: array items: type: string succession_stage: type: string heavy_metal_tolerance: type: boolean wikidata_qid: type: string gbif_id: type: string eppo_code: type: string pfaf_url: type: string wild_bee_count: type: integer wild_bee_specialist_count: type: integer butterfly_moth_count: type: integer caterpillar_host_count: type: integer caterpillar_specialist_count: type: integer hoverfly_count: type: integer beetle_count: type: integer bird_count: type: integer mammal_count: type: integer naturadb_tags: type: string source_urls: type: array items: type: string PaginatedSpecies: type: object properties: data: type: array items: $ref: "#/components/schemas/Species" total: type: integer page: type: integer per_page: type: integer # ── Cultivar ──────────────────────────────────────────────────── Cultivar: type: object description: A named cultivar/variety with growing parameters and scheduling properties: id: type: string format: uuid slug: type: string species_id: type: string format: uuid name: type: string name_en: type: string nullable: true name_de: type: string nullable: true name_scientific: type: string nullable: true description: type: string nullable: true description_de: type: string nullable: true description_en: type: string nullable: true is_organic: type: boolean perennial: type: boolean growing_time_days: type: integer nullable: true planting_depth_cm: type: number nullable: true row_spacing_cm: type: number nullable: true plant_spacing_cm: type: number nullable: true days_to_germination: type: integer nullable: true germination_temp_c: type: number nullable: true light_requirement: type: string nullable: true stratification_required: type: boolean nullable: true stratification_days: type: integer nullable: true scarification_required: type: boolean nullable: true seed_viability_years: type: integer nullable: true storage_temp_c: type: number nullable: true storage_humidity: type: string nullable: true storage_notes: type: string nullable: true min_temp: type: number nullable: true max_temp: type: number nullable: true humidity: type: string nullable: true light: type: string nullable: true frost_tolerance: type: string nullable: true min_light_hours_day: type: number nullable: true optimal_light_hours_day: type: number nullable: true greenhouse_min_temp_c: type: number nullable: true indoor_season_extension_weeks: type: integer nullable: true ventilation_requirement: type: string nullable: true heating_required: type: boolean nullable: true indoor_sowing_months: type: array items: type: integer nullable: true description: Month numbers (1-12) direct_sowing_months: type: array items: type: integer nullable: true transplanting_months: type: array items: type: integer nullable: true glasshouse_months: type: array items: type: integer nullable: true harvesting_months: type: array items: type: integer nullable: true indoor_sowing_weeks: type: array items: type: integer nullable: true description: ISO week numbers (1-52) direct_sowing_weeks: type: array items: type: integer nullable: true transplanting_weeks: type: array items: type: integer nullable: true glasshouse_weeks: type: array items: type: integer nullable: true harvesting_weeks: type: array items: type: integer nullable: true succession_planting_days: type: integer nullable: true planting_notes: type: string nullable: true propagation_methods: type: array items: type: string nullable: true cutting_season: type: string nullable: true rootstock_species_id: type: string format: uuid nullable: true years_to_first_harvest: type: integer nullable: true productive_lifespan_years: type: integer nullable: true expected_yield_kg_per_m2: type: number nullable: true yield_unit: type: string nullable: true expected_yield_value: type: number nullable: true harvest_window_days: type: integer nullable: true storage_method: type: array items: type: string nullable: true shelf_life_days: type: integer nullable: true cold_storage_days: type: integer nullable: true pollination_group: type: string nullable: true self_fertile: type: boolean nullable: true rootstock_compatibility: type: string nullable: true wikidata_qid: type: string nullable: true gbif_id: type: string nullable: true pfaf_url: type: string nullable: true primary_image_key: type: string nullable: true source_urls: type: array items: type: string nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time CreateCultivar: type: object required: [species_id, name] properties: species_id: type: string format: uuid name: type: string name_en: type: string name_de: type: string name_scientific: type: string description: type: string description_de: type: string description_en: type: string is_organic: type: boolean perennial: type: boolean growing_time_days: type: integer planting_depth_cm: type: number row_spacing_cm: type: number plant_spacing_cm: type: number days_to_germination: type: integer germination_temp_c: type: number light_requirement: type: string indoor_sowing_months: type: array items: type: integer direct_sowing_months: type: array items: type: integer transplanting_months: type: array items: type: integer glasshouse_months: type: array items: type: integer harvesting_months: type: array items: type: integer indoor_sowing_weeks: type: array items: type: integer direct_sowing_weeks: type: array items: type: integer transplanting_weeks: type: array items: type: integer glasshouse_weeks: type: array items: type: integer harvesting_weeks: type: array items: type: integer source_urls: type: array items: type: string PaginatedCultivars: type: object properties: data: type: array items: $ref: "#/components/schemas/Cultivar" total: type: integer page: type: integer per_page: type: integer # ── Supplier ──────────────────────────────────────────────────── Supplier: type: object properties: id: type: string format: uuid slug: type: string name: type: string url: type: string nullable: true is_organic: type: boolean is_demeter: type: boolean country: type: string nullable: true notes: type: string nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time CreateSupplier: type: object required: [name] properties: name: type: string url: type: string is_organic: type: boolean is_demeter: type: boolean country: type: string notes: type: string CultivarSupplier: type: object properties: id: type: string format: uuid cultivar_id: type: string format: uuid supplier_id: type: string format: uuid article_number: type: string nullable: true product_url: type: string nullable: true price_eur: type: number nullable: true pack_size: type: number nullable: true pack_unit: type: string nullable: true last_checked_at: type: string format: date-time nullable: true created_at: type: string format: date-time CreateCultivarSupplier: type: object required: [supplier_id] properties: supplier_id: type: string format: uuid article_number: type: string product_url: type: string price_eur: type: number pack_size: type: number pack_unit: type: string # ── Companions ────────────────────────────────────────────────── CompanionRelationship: type: object properties: id: type: string format: uuid species_a_id: type: string format: uuid species_b_id: type: string format: uuid relationship: type: string description: "e.g. beneficial, antagonistic, neutral" mechanism: type: string nullable: true source_url: type: string nullable: true created_at: type: string format: date-time CompanionWithNames: type: object description: Companion relationship with resolved species names and images properties: id: type: string format: uuid species_a_id: type: string format: uuid species_b_id: type: string format: uuid relationship: type: string mechanism: type: string nullable: true source_url: type: string nullable: true created_at: type: string format: date-time species_a_scientific: type: string species_a_de: type: string nullable: true species_a_slug: type: string species_a_image: type: string nullable: true species_b_scientific: type: string species_b_de: type: string nullable: true species_b_slug: type: string species_b_image: type: string nullable: true CreateCompanion: type: object required: [species_a_id, species_b_id, relationship] properties: species_a_id: type: string format: uuid species_b_id: type: string format: uuid relationship: type: string mechanism: type: string source_url: type: string # ── Images ────────────────────────────────────────────────────── Image: type: object properties: id: type: string format: uuid entity_type: type: string entity_id: type: string format: uuid s3_key: type: string caption: type: string nullable: true source_url: type: string nullable: true license: type: string nullable: true is_primary: type: boolean created_at: type: string format: date-time # ── Search ────────────────────────────────────────────────────── SearchResult: type: object properties: entity_type: type: string description: "family, species, or cultivar" id: type: string format: uuid slug: type: string name: type: string description: type: string nullable: true rank: type: number description: Full-text search relevance score # ── Auth ──────────────────────────────────────────────────────── MeResponse: type: object properties: id: type: string format: uuid email: type: string name: type: string nullable: true nickname: type: string nullable: true admin: type: boolean