openapi: 3.1.0
info:
  title: Dreeve HTTP API
  version: v1
  summary: The public HTTP API of a self-hosted Dreeve instance.
  description: |
    Every endpoint is authenticated with a bearer token and returns JSON. Errors always use the
    same shape, with a stable `error` code to branch on and a human readable `message`.

    The raw OpenAPI document is available at [/api/openapi.yaml](/api/openapi.yaml), for use with
    code generators, API clients and AI assistants.

    ## Enabling the API

    The API is closed until `DREEVE_API_KEY` is set. Generate a key in the admin panel under
    **Settings → Security → API access**, put it in your `.env` file, and recreate your containers

    Only a key generated by Dreeve is accepted; it looks like `drv_` followed by 64 hex characters.
    To revoke a key, delete the line and recreate the containers again.

    `GET /api/v1/status` is the cheapest way to check that a URL and key are correct.

servers:
  - url: '{appUrl}'
    description: Your Dreeve instance.
    variables:
      appUrl:
        default: http://localhost:8080
        description: The value you configured as APP_URL
security:
  - apiKey: []
tags:
  - name: Status
  - name: Activities
  - name: Activity images
  - name: File imports
  - name: Gear
  - name: Weight
paths:
  /api/v1/status:
    get:
      operationId: getStatus
      tags: [Status]
      summary: Check the connection
      responses:
        '200':
          description: The key is valid.
          content:
            application/json:
              schema:
                type: object
                required: [app, version, canUpload]
                properties:
                  app:
                    type: string
                    const: dreeve
                  version:
                    type: string
                    description: The running Dreeve version.
                    examples: ['v5.2.3']
                  canUpload:
                    type: boolean
                    description: >-
                      Whether this instance accepts uploads. False when `IMPORT_MODE` is not
                      `files`, in which case every upload answers 409.
              examples:
                filesMode:
                  summary: Ready to accept uploads
                  value: { app: dreeve, version: v5.2.3, canUpload: true }
                stravaApiMode:
                  summary: Valid key, but uploads are disabled
                  value: { app: dreeve, version: v5.2.3, canUpload: false }
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/activity/upload:
    post:
      operationId: uploadActivityFile
      tags: [File imports]
      summary: Upload an activity file
      description: |
        Queues a `.fit`, `.tcx` or `.gpx` file for import.

        **The upload is asynchronous**: a success is `202 Accepted`, not `200 OK`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The activity file. Must be named `*.fit`, `*.tcx` or `*.gpx`.
      responses:
        '202':
          description: The file was queued for import.
          content:
            application/json:
              schema:
                type: object
                required: [status, message]
                properties:
                  status:
                    type: string
                    const: queued
                  message:
                    type: string
              examples:
                queued:
                  value:
                    status: queued
                    message: File queued for import.
        '400':
          description: No usable `file` part was sent.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                missingFile:
                  value: { error: missing_file, message: 'A "file" part is required.' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: >-
            This instance runs with `IMPORT_MODE=stravaApi`, so it cannot accept uploaded files.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                wrongImportMode:
                  value:
                    error: import_mode_not_files
                    message: Activity files can only be uploaded when running in file import mode.
        '413':
          description: >-
            The file is larger than `upload_max_filesize`, or the whole request is larger than
            `post_max_size`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                tooLarge:
                  value: { error: file_too_large, message: The uploaded file is too large. }
        '415':
          description: The body was not `multipart/form-data`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                notMultipart:
                  value:
                    error: unsupported_media_type
                    message: Send the file as multipart/form-data.
        '422':
          description: >-
            The filename is unusable. Either the extension is not one of `fit`, `tcx` or `gpx`, or
            the name itself contains control characters or invalid UTF-8.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                wrongExtension:
                  value: { error: unsupported_file_type, message: The file type is not supported. }
        '500':
          description: The upload could not be stored.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                cannotStore:
                  value: { error: internal_error, message: The upload could not be stored. }
  /api/v1/activities:
    get:
      operationId: listActivities
      tags: [Activities]
      summary: Search activities
      description: |
        Returns activities newest first. All filters are optional

        ```
        GET /api/v1/activities?filters[from]=2023-09-04&filters[to]=2023-09-04&pagination[size]=50
        ```
      parameters:
        - name: filters
          in: query
          required: false
          style: deepObject
          explode: true
          schema:
            type: object
            properties:
              from:
                type: string
                format: date
                description: >-
                  Only activities that started on this day or later
                examples: ['2023-09-04']
              to:
                type: string
                format: date
                description: >-
                  Only activities that started on this day or earlier
                examples: ['2023-09-11']
              sportType:
                type: string
                description: >-
                  One sport type, or several separated by commas
                examples: ['Ride', 'Run,TrailRun,Hike']
              hasGpx:
                type: string
                enum: ['true', 'false']
        - name: pagination
          in: query
          required: false
          style: deepObject
          explode: true
          schema:
            type: object
            properties:
              page:
                type: integer
                minimum: 1
                default: 1
              size:
                type: integer
                minimum: 1
                maximum: 100
                default: 25
      responses:
        '200':
          description: A page of activities, newest first.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ActivityList' }
        '400':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                unknownSportType:
                  value:
                    error: bad_request
                    message: '"filters[sportType]" contains an unknown sport type.'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/activities/{activityId}:
    get:
      operationId: getActivity
      tags: [Activities]
      summary: Get one activity
      parameters:
        - $ref: '#/components/parameters/ActivityId'
      responses:
        '200':
          description: The activity.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Activity' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ActivityNotFound'
    patch:
      operationId: updateActivity
      tags: [Activities]
      summary: Update an activity
      description: >-
        Changes the name, sport type, description or gear of an activity.
      parameters:
        - $ref: '#/components/parameters/ActivityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              additionalProperties: false
              properties:
                name:
                  type: string
                  minLength: 1
                  examples: ['Evening ride']
                sportType:
                  type: string
                  examples: ['GravelRide']
                description:
                  type: [string, 'null']
                  description: Send `null` or an empty string to clear the description.
                  examples: ['Rerouted around the closed bridge']
                gearId:
                  type: [string, 'null']
                  description: The prefixed id of existing gear. Send `null` or an empty string to remove the gear.
                  examples: ['gear-b12659861']
      responses:
        '200':
          description: The updated activity.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Activity' }
        '400':
          description: The JSON body is invalid, empty, or contains an unknown field or value.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                invalidSportType:
                  value: { error: bad_request, message: 'A valid "sportType" is required.' }
                unknownGear:
                  value: { error: bad_request, message: 'Gear "gear-b12659861" not found.' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ActivityNotFound'
        '409':
          description: >-
            This instance runs with `IMPORT_MODE=stravaApi`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                wrongImportMode:
                  value:
                    error: import_mode_not_files
                    message: Activities can only be updated when running in file import mode.
    delete:
      operationId: deleteActivity
      tags: [Activities]
      summary: Delete an activity
      description: >-
        Deletes the activity together with its streams, laps, splits, best efforts and import log.
      parameters:
        - $ref: '#/components/parameters/ActivityId'
      responses:
        '204':
          description: The activity was deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ActivityNotFound'
        '409':
          description: >-
            This instance runs with `IMPORT_MODE=stravaApi`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                wrongImportMode:
                  value:
                    error: import_mode_not_files
                    message: Activities can only be deleted when running in file import mode.
  /api/v1/activities/{activityId}/gpx:
    get:
      operationId: getActivityGpx
      tags: [Activities]
      summary: Download as GPX
      description: >-
        Only activities recorded with GPS data can be exported. Check `hasGpx` on the activity
        first
      parameters:
        - $ref: '#/components/parameters/ActivityId'
      responses:
        '200':
          description: The route as a GPX 1.1 document.
          headers:
            Content-Disposition:
              schema:
                type: string
                examples: ['attachment; filename=2023-09-04-evening-ride.gpx']
          content:
            application/gpx+xml:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: The activity does not exist, or it exists but has no GPS data.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                unknownActivity:
                  summary: No such activity
                  value: { error: not_found, message: 'Activity "activity-1" not found' }
                withoutGps:
                  summary: A manual or indoor activity
                  value:
                    error: gpx_not_available
                    message: 'Activity "activity-9542782314" has no GPS or time data to export as GPX.'
  /api/v1/activities/{activityId}/images:
    post:
      operationId: addActivityImage
      tags: [Activity images]
      summary: Add an image
      description: >-
        Stores the image and appends it to the activity's `images`.
      parameters:
        - $ref: '#/components/parameters/ActivityId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: >-
                    A jpg, png or webp image.
      responses:
        '201':
          description: The image was added to the activity.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ActivityImage' }
        '400':
          description: No usable `file` part was sent.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                missingFile:
                  value: { error: missing_file, message: 'A "file" part is required.' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ActivityNotFound'
        '409':
          description: >-
            This instance runs with `IMPORT_MODE=stravaApi`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                wrongImportMode:
                  value:
                    error: import_mode_not_files
                    message: Images can only be added to activities when running in file import mode.
        '413':
          description: >-
            The file is larger than `upload_max_filesize`, or the whole request is larger than
            `post_max_size`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                tooLarge:
                  value: { error: file_too_large, message: The uploaded file is too large. }
        '415':
          description: The body was not `multipart/form-data`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                notMultipart:
                  value:
                    error: unsupported_media_type
                    message: Send the file as multipart/form-data.
        '422':
          description: The file is not a jpg, png or webp image.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                notAnImage:
                  value:
                    error: unsupported_file_type
                    message: The file is not a jpg, png or webp image.
        '500':
          description: The upload could not be stored.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                cannotStore:
                  value: { error: internal_error, message: The upload could not be stored. }
  /api/v1/activities/{activityId}/images/{imageId}:
    delete:
      operationId: removeActivityImage
      tags: [Activity images]
      summary: Remove an image
      description: Removes the image from the activity and deletes the file.
      parameters:
        - $ref: '#/components/parameters/ActivityId'
        - $ref: '#/components/parameters/ActivityImageId'
      responses:
        '204':
          description: The image was removed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: The activity does not exist, or the image is not one of its images.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                unknownActivity:
                  summary: No such activity
                  value: { error: not_found, message: 'Activity "activity-1" not found' }
                unknownImage:
                  summary: No such image on this activity
                  value:
                    error: not_found
                    message: 'Image "activityImage-0a8cdfbb-63b4-4f0f-a09d-faedd5a00753" not found'
        '409':
          description: >-
            This instance runs with `IMPORT_MODE=stravaApi`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                wrongImportMode:
                  value:
                    error: import_mode_not_files
                    message: Images can only be removed from activities when running in file import mode.
  /api/v1/file-imports:
    get:
      operationId: listFileImports
      tags: [File imports]
      summary: Search file imports
      description: |
        Lists the files that are waiting to be imported, followed by the outcome of every file
        import, newest first.

        | Status    | Meaning                                                                     |
        |-----------|-----------------------------------------------------------------------------|
        | `queued`  | The file is waiting for the next import run.                                |
        | `success` | The file was imported; `activityId` is the new activity.                    |
        | `skipped` | The file duplicates an activity that was already imported.                  |
        | `failed`  | The file could not be imported; `errorMessage` says why.                    |

        ```
        GET /api/v1/file-imports?filters[filename]=morning-ride.fit
        ```
      parameters:
        - name: filters
          in: query
          required: false
          style: deepObject
          explode: true
          schema:
            type: object
            properties:
              filename:
                type: string
                description: >-
                  Only imports of the file with exactly this name
                examples: ['morning-ride.fit']
              status:
                type: string
                description: >-
                  One status, or several separated by commas. Queued files are always listed.
                examples: ['failed', 'skipped,failed']
              source:
                type: string
                description: >-
                  One source, or several separated by commas
                examples: ['fitFile', 'tcxFile,gpxFile']
        - name: pagination
          in: query
          required: false
          style: deepObject
          explode: true
          schema:
            type: object
            properties:
              page:
                type: integer
                minimum: 1
                default: 1
              size:
                type: integer
                minimum: 1
                maximum: 100
                default: 25
      responses:
        '200':
          description: A page of file imports, queued files first, then newest first.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FileImportList' }
        '400':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              examples:
                unknownStatus:
                  value:
                    error: bad_request
                    message: '"filters[status]" contains an unknown status.'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/gear:
    get:
      operationId: listGear
      tags: [Gear]
      summary: List gear
      description: >-
        Lists all gear, active gear first
      responses:
        '200':
          description: All gear.
          content:
            application/json:
              schema:
                type: object
                required: [gear]
                properties:
                  gear:
                    type: array
                    items: { $ref: '#/components/schemas/Gear' }
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/athlete/weights:
    get:
      operationId: listAthleteWeights
      tags: [Weight]
      summary: List weight measurements
      responses:
        '200':
          description: Weight measurements, ordered from newest to oldest.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WeightHistory' }
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: recordAthleteWeight
      tags: [Weight]
      summary: Record a weight measurement
      description: >-
        Creates a weight measurement for a calendar date, or replaces the existing measurement for
        that date.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [weight, on]
              properties:
                weight:
                  type: number
                  exclusiveMinimum: 0
                  description: >-
                    The measurement in the configured Appearance unit system: kilograms for Metric
                    and pounds for Imperial.
                  examples: [71.4]
                on:
                  type: string
                  format: date
                  description: The measurement date as `YYYY-MM-DD`.
                  examples: ['2026-09-08']
      responses:
        '200':
          description: The measurement for the date was recorded.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WeightRecorded' }
        '400':
          description: The JSON body, date, or weight is invalid.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '415':
          description: The body was not `application/json`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /api/v1/athlete/weights/{on}:
    delete:
      operationId: deleteAthleteWeight
      tags: [Weight]
      summary: Delete a weight measurement
      parameters:
        - name: on
          in: path
          required: true
          schema:
            type: string
            format: date
      responses:
        '204':
          description: The measurement was deleted, if one existed for the date.
        '400':
          description: The date is invalid.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        Send your key as `Authorization: Bearer drv_...`
  schemas:
    ActivityList:
      type: object
      required: [activities, pagination]
      properties:
        activities:
          type: array
          items: { $ref: '#/components/schemas/Activity' }
        pagination: { $ref: '#/components/schemas/PaginationMeta' }
    FileImportList:
      type: object
      required: [fileImports, pagination]
      properties:
        fileImports:
          type: array
          items: { $ref: '#/components/schemas/FileImport' }
        pagination: { $ref: '#/components/schemas/PaginationMeta' }
    FileImport:
      type: object
      required: [id, filename, source, status, errorMessage, activityId, importedOn]
      properties:
        id:
          type: [string, 'null']
          description: Null while the file is queued.
          examples: ['fileImport-0198a2b1-8f3e-7c2d-9a4b-1f2e3d4c5b6a']
        filename:
          type: string
          examples: ['morning-ride.fit']
        source:
          type: string
          enum: [fitFile, tcxFile, gpxFile]
        status:
          type: string
          enum: [queued, success, skipped, failed]
        errorMessage:
          type: [string, 'null']
          examples: ['Skipped, activity was already imported']
        activityId:
          type: [string, 'null']
          description: The imported activity. Only set when the status is `success`.
          examples: ['activity-9542782314']
        importedOn:
          type: [string, 'null']
          format: date-time
          description: Null while the file is queued.
          examples: ['2026-09-28T10:00:00']
    Gear:
      type: object
      required: [id, name, type, isRetired, distanceInMeter, numberOfActivities]
      properties:
        id:
          type: string
          examples: ['gear-b12659861']
        name:
          type: string
          examples: ['Canyon Grail']
        type:
          type: string
          enum: [imported, custom]
        isRetired:
          type: boolean
        distanceInMeter:
          type: integer
          examples: [10500]
        numberOfActivities:
          type: integer
          examples: [1]
    PaginationMeta:
      type: object
      required: [page, size, total, totalPages]
      properties:
        page:
          type: integer
        size:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer
    Activity:
      type: object
      required:
        - id
        - name
        - description
        - sportType
        - worldType
        - importSource
        - startDateLocal
        - distanceInMeter
        - elevationInMeter
        - movingTimeInSeconds
        - elapsedTimeInSeconds
        - averageSpeedInKmPerHour
        - maxSpeedInKmPerHour
        - isCommute
        - location
        - passedThroughCountries
        - hasGpx
        - images
      properties:
        id:
          type: string
          examples: ['activity-9756441741']
        name:
          type: string
          examples: ['Evening Ride']
        description:
          type: string
        sportType:
          type: string
          description: Strava's sport type vocabulary.
          examples: ['Ride', 'TrailRun', 'Hike']
        workoutType:
          type: [string, 'null']
        worldType:
          type: string
          examples: ['realWorld', 'zwift']
        importSource:
          type: string
          examples: ['stravaApi', 'file', 'manual']
        startDateLocal:
          type: string
          examples: ['2023-09-04T21:05:38']
        distanceInMeter:
          type: integer
        elevationInMeter:
          type: integer
        movingTimeInSeconds:
          type: integer
        elapsedTimeInSeconds:
          type: integer
        averageSpeedInKmPerHour:
          type: number
        maxSpeedInKmPerHour:
          type: number
        calories:
          type: [integer, 'null']
        kilojoules:
          type: [integer, 'null']
        averageHeartRate:
          type: [integer, 'null']
        maxHeartRate:
          type: [integer, 'null']
        averagePower:
          type: [integer, 'null']
        maxPower:
          type: [integer, 'null']
        averageCadence:
          type: [integer, 'null']
        isCommute:
          type: boolean
        deviceName:
          type: [string, 'null']
        gearId:
          type: [string, 'null']
          examples: ['gear-b12659861']
        location: { $ref: '#/components/schemas/ActivityLocation' }
        passedThroughCountries:
          type: array
          description: Lowercased ISO 3166-1 alpha-2 codes, empty when not reverse geocoded.
          items:
            type: string
          examples: [['be', 'nl']]
        startLatLng:
          type: [array, 'null']
          description: The starting point as `[latitude, longitude]`, null without GPS data.
          minItems: 2
          maxItems: 2
          items:
            type: number
          examples: [[51.2, 3.18]]
        encodedPolyline:
          type: [string, 'null']
        hasGpx:
          type: boolean
          description: >-
            Whether `GET /api/v1/activities/{activityId}/gpx` will return a file for this activity.
        images:
          type: array
          items: { $ref: '#/components/schemas/ActivityImage' }
    ActivityImage:
      type: object
      required: [id, url]
      properties:
        id:
          type: string
          examples: ['activityImage-0a8cdfbb-63b4-4f0f-a09d-faedd5a00753']
        url:
          type: string
          format: uri
          description: >-
            Where the image is served. With demo mode enabled, visitors who are not logged in get a
            placeholder photo instead.
          examples: ['https://dreeve.example.com/files/activities/0a8cdfbb-63b4-4f0f-a09d-faedd5a00753.jpg']
    ActivityLocation:
      type: object
      required: [countryCode, state, city]
      properties:
        countryCode:
          type: [string, 'null']
          description: Lowercased ISO 3166-1 alpha-2.
          examples: ['be']
        state:
          type: [string, 'null']
          examples: ['West-Vlaanderen']
        city:
          type: [string, 'null']
          examples: ['Brugge']
    WeightHistory:
      type: object
      required: [weights]
      properties:
        weights:
          type: array
          items: { $ref: '#/components/schemas/WeightMeasurement' }
    WeightMeasurement:
      type: object
      required: [on, weight]
      properties:
        on:
          type: string
          format: date
        weight:
          type: number
          description: >-
            The measurement in the configured Appearance unit system: kilograms for Metric and
            pounds for Imperial.
    WeightRecorded:
      type: object
      required: [on, weight]
      properties:
        on:
          type: string
          format: date
        weight:
          type: number
          description: >-
            The measurement in the configured Appearance unit system: kilograms for Metric and
            pounds for Imperial.
    Error:
      type: object
      required: [error, message]
      description: Every error uses this shape.
      properties:
        error:
          type: string
          description: >-
            A stable, machine readable code.
          enum:
            - invalid_token
            - missing_file
            - unsupported_media_type
            - unsupported_file_type
            - file_too_large
            - import_mode_not_files
            - gpx_not_available
            - not_found
            - method_not_allowed
            - bad_request
            - forbidden
            - internal_error
        message:
          type: string
  parameters:
    ActivityId:
      name: activityId
      in: path
      required: true
      description: The prefixed activity identifier, `activity-` included.
      schema:
        type: string
        examples: ['activity-9756441741']
    ActivityImageId:
      name: imageId
      in: path
      required: true
      description: The prefixed image identifier, `activityImage-` included, as listed in the activity's `images`.
      schema:
        type: string
        examples: ['activityImage-0a8cdfbb-63b4-4f0f-a09d-faedd5a00753']
  responses:
    ActivityNotFound:
      description: No activity with this identifier exists.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            unknownActivity:
              value: { error: not_found, message: 'Activity "activity-1" not found' }
    Unauthorized:
      description: >-
        The `Authorization` header was missing, malformed, or did not match the configured key.
      headers:
        WWW-Authenticate:
          schema:
            type: string
            examples: ['Bearer realm="Dreeve"']
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            invalidToken:
              value:
                error: invalid_token
                message: A valid API token is required. Send it in the Authorization header as a Bearer token.
