openapi: 3.1.0
info:
  title: Open Food Facts Nutrition API
  version: 0.1.0
  description: Query-oriented access to an Open Food Facts Parquet dump.
servers:
  - url: https://nutrition.sh
    description: Public instance
  - url: http://127.0.0.1:4567
    description: Local instance
paths:
  /health:
    get:
      operationId: health
      responses:
        '200':
          description: Service and import status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
  /api/v1:
    get:
      operationId: apiMetadata
      responses:
        '200':
          description: Dataset metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiMetadata'
  /api/v1/products:
    get:
      operationId: listProducts
      parameters:
        - { name: query, in: query, schema: { type: string, maxLength: 200 } }
        - { name: brand, in: query, schema: { type: string, maxLength: 200 } }
        - { name: category, in: query, schema: { type: string, maxLength: 200 } }
        - { name: country, in: query, schema: { type: string, maxLength: 200 } }
        - { name: label, in: query, schema: { type: string, maxLength: 200 } }
        - { name: allergen, in: query, schema: { type: string, maxLength: 200 } }
        - { name: nutriscore_grade, in: query, schema: { type: string } }
        - { name: nova_group, in: query, schema: { type: integer, minimum: 1, maximum: 4 } }
        - { name: nutrient, in: query, schema: { type: string, maxLength: 200 } }
        - { name: min_per_100g, in: query, schema: { type: number } }
        - { name: max_per_100g, in: query, schema: { type: number } }
        - { name: cursor, in: query, schema: { type: string, maxLength: 200 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
      responses:
        '200':
          description: Product page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductList'
        '400':
          $ref: '#/components/responses/BadRequest'
  /api/v1/products/{code}:
    get:
      operationId: getProduct
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
      responses:
        '200':
          description: Complete normalized product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductDetail'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/nutrients:
    get:
      operationId: listNutrients
      parameters:
        - { name: query, in: query, schema: { type: string, maxLength: 100 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        '200':
          description: Nutrient catalog
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/NutrientCatalogItem' }
components:
  responses:
    BadRequest:
      description: Invalid query parameters
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Product not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Health:
      type: object
      required: [status, product_count, import_completed]
      properties:
        status: { type: string, const: ok }
        product_count: { type: integer }
        import_completed: { type: boolean }
    ApiMetadata:
      type: object
      required: [schema_version, product_count, import_completed]
      properties:
        schema_version: { type: string }
        product_count: { type: integer }
        import_completed: { type: boolean }
    ProductSummary:
      type: object
      required: [code]
      properties:
        code: { type: string }
        name: { type: [string, 'null'] }
        brands: { type: [string, 'null'] }
        quantity: { type: [string, 'null'] }
        serving_size: { type: [string, 'null'] }
        language: { type: [string, 'null'] }
        nutriscore_grade: { type: [string, 'null'] }
        nutriscore_score: { type: [integer, 'null'] }
        nova_group: { type: [integer, 'null'] }
        environmental_score_grade: { type: [string, 'null'] }
        environmental_score_score: { type: [integer, 'null'] }
        completeness: { type: [number, 'null'] }
        last_modified_at: { type: [integer, 'null'] }
    ProductList:
      type: object
      required: [data, page]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/ProductSummary' }
        page:
          type: object
          required: [limit, next_cursor]
          properties:
            limit: { type: integer }
            next_cursor: { type: [string, 'null'] }
    ProductDetail:
      allOf:
        - $ref: '#/components/schemas/ProductSummary'
        - type: object
          required: [localized_texts, nutrients, tags]
          properties:
            generic_name: { type: [string, 'null'] }
            ingredients: { type: [string, 'null'] }
            ingredients_text: { type: [string, 'null'] }
            scans: { type: [integer, 'null'] }
            unique_scans: { type: [integer, 'null'] }
            no_nutrition_data: { type: boolean }
            obsolete: { type: boolean }
            created_at: { type: [integer, 'null'] }
            last_updated_at: { type: [integer, 'null'] }
            localized_texts:
              type: array
              items:
                type: object
                required: [kind, language, text]
                properties:
                  kind: { type: string }
                  language: { type: string }
                  text: { type: string }
            nutrients:
              type: array
              items: { $ref: '#/components/schemas/Nutrient' }
            tags:
              type: object
              additionalProperties:
                type: array
                items: { type: string }
    Nutrient:
      type: object
      required: [name]
      properties:
        name: { type: string }
        unit:
          type: [string, 'null']
          description: Unit of `value`, the amount as labeled on the package.
        value: { type: [number, 'null'] }
        per_100g_unit:
          type: [string, 'null']
          description: >-
            Unit of `per_100g` and `per_serving`. Grams for mass nutrients
            (mg, µg, IU and percent-daily-value entries are converted), kJ for
            `energy`, kcal for `energy-kcal`; other units pass through.
        per_100g: { type: [number, 'null'] }
        per_serving: { type: [number, 'null'] }
        prepared_value: { type: [number, 'null'] }
        prepared_per_100g: { type: [number, 'null'] }
        prepared_per_serving: { type: [number, 'null'] }
        prepared_unit: { type: [string, 'null'] }
    NutrientCatalogItem:
      type: object
      required: [name, product_count]
      properties:
        name: { type: string }
        unit: { type: [string, 'null'] }
        product_count: { type: integer }
        minimum_per_100g: { type: [number, 'null'] }
        maximum_per_100g: { type: [number, 'null'] }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
