{
  "openapi": "3.0.3",
  "info": {
    "title": "Jellyfish REST API",
    "version": "0.23.1",
    "description": "HTTP/JSON APIs exposed by the Jellyfish backend. Most dashboard/control flows are exposed via the GraphQL endpoint (`/graphql`); the routes documented here handle authentication, passkeys, robot telemetry ingest, S3 signed URLs, and PDF report generation.",
    "license": {
      "name": "MIT",
      "url": "https://github.com/clearbothk/jellyfish/blob/main/LICENSE"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "Same host as the GraphQL/WebSocket endpoints."
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Login, logout, password, and passkey flows."
    },
    {
      "name": "Robot",
      "description": "Robot-side endpoints (require a `crat-…` token)."
    },
    {
      "name": "Dashboard",
      "description": "Operator endpoints (require a user session)."
    },
    {
      "name": "Cloud",
      "description": "Endpoints used by robots to upload telemetry."
    },
    {
      "name": "Report",
      "description": "PDF report generation."
    }
  ],
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session-token",
        "description": "Session token returned by the user login endpoints. Alternatively pass the same value as a `Authorization` header."
      },
      "robotBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "crat-…",
        "description": "Robot authorization token. Must be prefixed with `crat-` and sent in the `Authorization` header."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "success",
          "message"
        ]
      },
      "SetPasswordRequest": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "One-time token from the email link."
          },
          "password": {
            "type": "string",
            "minLength": 5
          }
        },
        "required": [
          "token",
          "password"
        ]
      },
      "LoginRequest": {
        "type": "object",
        "properties": {
          "username": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string"
          }
        },
        "required": [
          "username",
          "password"
        ]
      },
      "LoginChallengeRequest": {
        "type": "object",
        "description": "Empty — challenge is generated server-side.",
        "properties": {}
      },
      "LoginChallengeResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "challengeId": {
            "type": "string",
            "format": "uuid"
          },
          "options": {
            "type": "object",
            "description": "WebAuthn `PublicKeyCredentialRequestOptions` returned by @simplewebauthn/server."
          }
        },
        "required": [
          "success",
          "challengeId",
          "options"
        ]
      },
      "LoginVerifyRequest": {
        "type": "object",
        "properties": {
          "challengeId": {
            "type": "string",
            "format": "uuid"
          },
          "credential": {
            "type": "object",
            "description": "WebAuthn `PublicKeyCredential` produced by the authenticator."
          }
        },
        "required": [
          "challengeId",
          "credential"
        ]
      },
      "LoginVerifyResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "token": {
            "type": "string",
            "description": "Session token to use as a bearer/cookie."
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              }
            }
          }
        },
        "required": [
          "success",
          "token",
          "user"
        ]
      },
      "ChangePasswordRequest": {
        "type": "object",
        "properties": {
          "password": {
            "type": "string",
            "minLength": 5
          }
        },
        "required": [
          "password"
        ]
      },
      "RobotAccessRequest": {
        "type": "object",
        "description": "Operation sent to the dashboard/AMQP broker for a robot owned by the authenticated user.",
        "properties": {
          "robotId": {
            "type": "integer"
          },
          "command": {
            "type": "string",
            "example": "on"
          }
        },
        "required": [
          "robotId",
          "command"
        ]
      },
      "SignedUrlRequest": {
        "type": "object",
        "properties": {
          "nameReceived": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "S3 object keys for which to generate signed URLs."
          }
        },
        "required": [
          "nameReceived"
        ]
      },
      "SignedUrlResponse": {
        "type": "array",
        "items": {
          "type": "string",
          "format": "uri"
        }
      },
      "InsertTrashRecordRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "S3 object key for the trash photo."
          },
          "robotId": {
            "type": "integer"
          },
          "gps": {
            "type": "object",
            "properties": {
              "lat": {
                "type": "number"
              },
              "lng": {
                "type": "number"
              }
            },
            "required": [
              "lat",
              "lng"
            ]
          },
          "categoryIds": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "confidences": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "Detection confidence (0-1) parallel to `categoryIds`."
          }
        },
        "required": [
          "name",
          "robotId",
          "gps",
          "categoryIds"
        ]
      },
      "AccumulateDistanceRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Robot id."
          },
          "distance": {
            "type": "number",
            "description": "Increment in metres to add to odometry."
          },
          "gps": {
            "type": "string",
            "description": "Free-form GPS string for the robot's last known location."
          }
        },
        "required": [
          "id",
          "distance"
        ]
      },
      "ControlRoomRegistrationRequest": {
        "type": "object",
        "properties": {
          "controlRoomId": {
            "type": "string"
          },
          "socketId": {
            "type": "string",
            "description": "Socket id of the dashboard session."
          }
        },
        "required": [
          "controlRoomId",
          "socketId"
        ]
      },
      "ControlRoom": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "controlRoomId": {
            "type": "string"
          },
          "socketId": {
            "type": "string"
          },
          "robotId": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "controlRoomId",
          "socketId",
          "robotId"
        ]
      },
      "RequestEMSDPDFRequest": {
        "type": "object",
        "properties": {
          "operationId": {
            "type": "integer"
          },
          "map": {
            "type": "string",
            "description": "Base64-encoded map PNG."
          }
        },
        "required": [
          "operationId",
          "map"
        ]
      },
      "RegisterChallengeRequest": {
        "type": "object",
        "description": "Empty — challenge is generated server-side from the session.",
        "properties": {}
      },
      "RegisterChallengeResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "challengeId": {
            "type": "string",
            "format": "uuid"
          },
          "options": {
            "type": "object",
            "description": "WebAuthn `PublicKeyCredentialCreationOptions` returned by @simplewebauthn/server."
          }
        },
        "required": [
          "success",
          "challengeId",
          "options"
        ]
      },
      "RegisterVerifyRequest": {
        "type": "object",
        "properties": {
          "challengeId": {
            "type": "string",
            "format": "uuid"
          },
          "credentials": {
            "type": "object",
            "description": "WebAuthn `RegistrationResponseJSON`."
          }
        },
        "required": [
          "challengeId",
          "credentials"
        ]
      },
      "RegisterCancelRequest": {
        "type": "object",
        "description": "Empty — server deletes any pending registration challenges for the session user.",
        "properties": {}
      },
      "CreateUserRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "username": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string",
            "minLength": 5
          }
        },
        "required": [
          "name",
          "username",
          "password"
        ]
      },
      "CreateUserResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          }
        },
        "required": [
          "id"
        ]
      }
    },
    "parameters": {
      "RobotIdParam": {
        "name": "robotId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        },
        "description": "Numeric robot id."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid credentials.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated user is not allowed to perform this action.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Internal server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/user/password/forget/set": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Set a new password using a forget-password token",
        "description": "Consumes a single-use `ForgetPassword.token` (sent via email) and updates\nthe user's password. The token row is deleted after a successful update.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPasswordRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password updated."
          },
          "400": {
            "description": "Invalid/expired token or re-used password."
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/user/password/new/set": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Accept an invitation and set the initial password",
        "description": "Consumes a JWT invitation token (signed with `JWT_SECRET`) and sets the\nuser's password. Marks `verified=true` and `invitationAcceptedAt=now`.\nIdempotent — repeats return `409`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPasswordRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password set successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid token payload."
          },
          "403": {
            "description": "Token expired."
          },
          "409": {
            "description": "Invitation already accepted."
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/user/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Email/password login",
        "description": "Returns a session token on success. The token must be sent in subsequent\nrequests as either the `Authorization` header or the `session-token` cookie.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session token.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "No matching user",
            "missing password": null,
            "or wrong password.": null
          }
        }
      }
    },
    "/user/login-challenge": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Begin a passkey (WebAuthn) login",
        "description": "Generates a `@simplewebauthn/server` authentication challenge, persists it\nto `WebAuthnChallenge` for 5 minutes, and returns the challenge id and\n`PublicKeyCredentialRequestOptions`.\n",
        "responses": {
          "200": {
            "description": "Challenge generated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginChallengeResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/user/login-verify": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Finish a passkey (WebAuthn) login",
        "description": "Verifies the authenticator response against the challenge and returns a\nsession token plus the authenticated user.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginVerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginVerifyResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing fields",
            "challenge invalid/expired": null,
            "or auth failed.": null
          },
          "404": {
            "description": "Passkey or user not found."
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/user/password/change": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Change the password for the current user",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePasswordRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "Missing/short password or reuse."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "User not found."
          },
          "409": {
            "description": "Password already changed."
          }
        }
      }
    },
    "/user/check": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Confirm the session is valid",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Session is valid."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/user/check/robot/{robotId}": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Confirm the session user can access a given robot",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RobotIdParam"
          }
        ],
        "responses": {
          "200": {
            "description": "User is authorized for this robot."
          },
          "401": {
            "description": "Unauthorised."
          }
        }
      }
    },
    "/user/robot/{robotId}/control_room": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Get the dashboard control-room binding for a robot",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RobotIdParam"
          }
        ],
        "responses": {
          "200": {
            "description": "Control-room record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlRoom"
                }
              }
            }
          },
          "400": {
            "description": "Control-room not configured."
          },
          "401": {
            "description": "Unauthorised for this robot."
          }
        }
      }
    },
    "/user/logout": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Invalidate the current session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Session deleted."
          },
          "400": {
            "description": "No authorization token provided."
          },
          "401": {
            "description": "No matching user session."
          },
          "500": {
            "description": "Error deleting the session."
          }
        }
      }
    },
    "/user/register-challenge": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Begin a passkey (WebAuthn) registration",
        "description": "Issues a registration challenge for the currently authenticated user and\nexpires any stale registration challenges. Excludes any credentials the\nuser has already registered.\n",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Challenge generated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterChallengeResponse"
                }
              }
            }
          },
          "401": {
            "description": "No active session."
          },
          "404": {
            "description": "User not found."
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/user/register-verify": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Finish a passkey (WebAuthn) registration",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterVerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Passkey registered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "Missing fields",
            "invalid/expired challenge": null,
            "or verification failed.": null
          },
          "404": {
            "description": "User not found."
          },
          "409": {
            "description": "Passkey already registered."
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/user/register-cancel": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Cancel an in-progress passkey registration",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pending registration challenges deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/user/create": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Create a new user (super-user only)",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUserResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid authorization token."
          },
          "401": {
            "description": "Missing authorization token."
          }
        }
      }
    },
    "/robot/check": {
      "get": {
        "tags": [
          "Robot"
        ],
        "summary": "Health check for authenticated robots",
        "security": [
          {
            "robotBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Token is valid."
          },
          "400": {
            "description": "Invalid authorization token."
          },
          "401": {
            "description": "Missing authorization header."
          },
          "403": {
            "description": "Authorization scheme must be `crat-…`."
          }
        }
      }
    },
    "/robot/control_room": {
      "get": {
        "tags": [
          "Robot"
        ],
        "summary": "Fetch the dashboard control-room binding for this robot",
        "security": [
          {
            "robotBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Control-room record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlRoom"
                }
              }
            }
          },
          "403": {
            "description": "No control-room is bound to this robot."
          }
        }
      }
    },
    "/robot/control_room/register": {
      "post": {
        "tags": [
          "Robot"
        ],
        "summary": "Register or update the dashboard control-room binding",
        "description": "Upserts the `ControlRoom` row linked to the authenticated robot, binding it\nto a dashboard session (`socketId`) and a stable `controlRoomId`.\n",
        "security": [
          {
            "robotBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ControlRoomRegistrationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated control-room row.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlRoom"
                }
              }
            }
          }
        }
      }
    },
    "/report/requestEMSDPDF": {
      "post": {
        "tags": [
          "Report"
        ],
        "summary": "Render the EMSD operation report PDF",
        "description": "Generates an EMSD-style PDF report for the given operation. The PDF is\nuploaded to S3 at `15/pdf/{operationId}.pdf`; a placeholder PDF is written\nfirst so the frontend can poll for completion, and replaced once the\nbackground render finishes.\n",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RequestEMSDPDFRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Render kicked off."
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/dashboard/remotePowerControl": {
      "post": {
        "tags": [
          "Dashboard"
        ],
        "summary": "Send a remote power-control command to a robot",
        "description": "Publishes `command` to the AMQP topic exchange with routing key\n`robot-{robotId}.remote-power`. The authenticated user must own the robot\nvia their organization membership.\n",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RobotAccessRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Command published."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/dashboard/remoteDocking": {
      "post": {
        "tags": [
          "Dashboard"
        ],
        "summary": "Send a remote docking command to a robot",
        "description": "Publishes `command` to the AMQP topic exchange with routing key\n`robot-{robotId}.remote-docking`. The authenticated user must own the robot\nvia their organization membership.\n",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RobotAccessRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Command published."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/cloud/getUrl": {
      "post": {
        "tags": [
          "Cloud"
        ],
        "summary": "Generate presigned S3 URLs",
        "description": "Returns a 1-hour presigned `GET` URL for each S3 object key supplied.\nFor keys that fail to sign, an empty string is returned in the same array position.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignedUrlRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed URLs (empty string per failure).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignedUrlResponse"
                }
              }
            }
          }
        }
      }
    },
    "/cloud/insertRecord": {
      "post": {
        "tags": [
          "Cloud"
        ],
        "summary": "Insert a trash detection record",
        "description": "Creates one `TrashRecord` row per detected category, attaching them to the\nrobot's currently running operation. The response is the array of\n`trashCategoryId` values in the same order as `categoryIds`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InsertTrashRecordRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created trash category ids.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `robotId`."
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/cloud/accumulateDistance": {
      "post": {
        "tags": [
          "Cloud"
        ],
        "summary": "Accumulate odometry and travel distance",
        "description": "Adds `distance` to the robot's lifetime `odometry`. If the robot currently has\nan active operation, the same distance is also added to `Operation.travelDistance`.\nOptionally sets `lastKnownLocation` if a `gps` string is supplied.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccumulateDistanceRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Distance accumulated."
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    }
  }
}