# Control via the REST API The REST API reads robot state and sends commands over HTTP. It is intended for application scripts, integrations with other systems, and quick checks through Swagger UI. Motion requests are synchronous: the response is returned after planning and execution finish or an internal timeout occurs. The API has no command queue. Wait for the current request to finish before sending another one. !!! warning "Safety" The REST API does not replace the standard KUKA safety system or emergency stop. Before the first run on a physical robot, check the Sunrise program, safety zones, tool, and workspace. Begin using the API in simulation. ## Starting and accessing the server The web server starts with the robot stack when the **web** section is enabled in the root **cobot-setting.yaml** file: ~~~ yaml web: enabled: true host: 0.0.0.0 port: 8007 token: "replace-with-a-long-secret-token" endpoints: pkg://iiwa_config/config/api_endpoints.yaml joint_limits: pkg://iiwa_config/config/moveit/joint_limits.yaml ~~~ After starting the stack with **cobot run**, the server is available at **http://server-address:8007**. Swagger UI shows the actual request schema and lets you run individual tests: - locally: [http://localhost:8007/docs](http://localhost:8007/docs); - from another computer: `http://server-address:8007/docs`; - OpenAPI JSON schema: `http://server-address:8007/openapi.json`. There is no separate health-check endpoint. If Swagger UI opens, the HTTP server is running. Readiness of ROS components is checked when a specific endpoint is called. When `host` is not `localhost`, `127.0.0.1`, or another loopback address, the `token` field is required; otherwise the server exits during startup. When `token` is set, Bearer authentication applies to every REST route and to the MCP route `/mcp/mcp`, including with a local `host`. The `/docs`, `/redoc`, and `/openapi.json` resources are available without a header so the browser can load Swagger UI. This does not expose control commands. Open `/docs`, click **Authorize**, paste the `web.token` value without the word `Bearer`, and confirm. Swagger adds the header to API requests. The token is stored in **cobot-setting.yaml**. Do not add it to documentation, scripts, or public repositories. If it is exposed, replace it and restart the stack. For remote access, also restrict port 8007 with a firewall or VPN. ### REST and MCP authentication Every request to a protected REST route or MCP must include the following header when `web.token` is set: ~~~ http Authorization: Bearer ~~~ Client setup examples: === "curl" ~~~ bash HOST=http://localhost:8007 API_TOKEN='copy web.token from cobot-setting.yaml' AUTH_HEADER="Authorization: Bearer ${API_TOKEN}" curl -sS -H "${AUTH_HEADER}" $HOST/robot/joint_states ~~~ === "Python" ~~~ python import httpx HOST = "http://localhost:8007" API_TOKEN = "copy web.token from cobot-setting.yaml" HEADERS = {"Authorization": f"Bearer {API_TOKEN}"} response = httpx.get(f"{HOST}/robot/joint_states", headers=HEADERS, timeout=10) response.raise_for_status() ~~~ === "MATLAB" ~~~ matlab HOST = 'http://localhost:8007'; API_TOKEN = 'copy web.token from cobot-setting.yaml'; readOpts = weboptions('Timeout', 10, ... 'HeaderFields', {'Authorization', ['Bearer ' API_TOKEN]}); jointState = webread([HOST '/robot/joint_states'], readOpts); ~~~ Pass the same header when connecting an MCP client to `http://:8007/mcp/mcp`. If the client supports custom HTTP headers, set `Authorization: Bearer ` in its connection settings. ## Preparing the examples The tabs on this page are synchronized. Select a language once and the same tab will be selected in subsequent examples. === "curl" ~~~ bash HOST=http://localhost:8007 ~~~ === "Python" ~~~ python import httpx HOST = "http://localhost:8007" T_READ = 10 T_MOVE = 60 ~~~ === "MATLAB" ~~~ matlab HOST = 'http://localhost:8007'; T_READ = 10; T_MOVE = 60; readOpts = weboptions('Timeout', T_READ); moveOpts = weboptions('MediaType', 'application/json', 'Timeout', T_MOVE); ~~~ MATLAB uses the built-in `webwrite` function for JSON requests. Uploading CSV and JSON files requires `matlab.net.http`, available in modern desktop versions of MATLAB. ## API overview | Method | Endpoint | Purpose | |---|---|---| | GET | /robot/joint_states | Current joint state | | GET | /robot/pose | TCP pose relative to base_link | | GET | /robot/positions | Named positions from the SRDF | | POST | /robot/move/named | Move to a named position | | POST | /robot/move/pose | Cartesian TCP motion | | POST | /robot/move/joints | Move the seven joints to specified angles | | POST | /trajectory/send | Publish a trajectory from JSON | | POST | /trajectory/send_csv | Upload and publish a trajectory from CSV | | GET | /trajectory/logs | Latest trajectory module log entries | | POST | /sequences/start | Start a sequence from a JSON file | | GET | /sequences/status | Running sequence status | | GET | /sequences/logs | Sequence process output | | POST | /stop | Stop API commands and the planner | ## Reading robot state ### Joint state GET **/robot/joint_states** returns the latest message from the ROS **/joint_states** topic. The **position**, **velocity**, and **effort** fields use the same order as the **name** array. Angles in **position** are in radians. === "curl" ~~~ bash curl -sS --max-time 10 $HOST/robot/joint_states | python3 -m json.tool ~~~ === "Python" ~~~ python response = httpx.get(f"{HOST}/robot/joint_states", timeout=T_READ) response.raise_for_status() state = response.json() print(dict(zip(state["name"], state["position"]))) ~~~ === "MATLAB" ~~~ matlab jointState = webread([HOST '/robot/joint_states'], readOpts); disp(jointState.position) ~~~ Typical response: ~~~ json { "name": ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6", "joint7"], "position": [0.0, 0.0, 0.0, -1.57, 0.0, 1.57, 0.0], "velocity": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], "effort": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] } ~~~ If no controller messages arrive within two seconds, the API returns 503. This usually means that the controller or robot has not started yet. ### TCP pose GET **/robot/pose** computes forward kinematics with the MoveIt **/compute_fk** service. Position is specified in meters. Orientation is returned as both a quaternion and Euler angles: - **euler_rad** — radians; - **euler_deg** — degrees; - **A, B, C** follow the KUKA ABC convention: rotation around Z, then Y, then X. === "curl" ~~~ bash curl -sS --max-time 10 $HOST/robot/pose | python3 -m json.tool ~~~ === "Python" ~~~ python response = httpx.get(f"{HOST}/robot/pose", timeout=T_READ) response.raise_for_status() pose = response.json() print(pose["position"]) ~~~ === "MATLAB" ~~~ matlab pose = webread([HOST '/robot/pose'], readOpts); fprintf('TCP: x=%.3f, y=%.3f, z=%.3f m\n', ... pose.position.x, pose.position.y, pose.position.z); fprintf('ABC: A=%.1f, B=%.1f, C=%.1f deg\n', ... pose.orientation.euler_deg.a, ... pose.orientation.euler_deg.b, ... pose.orientation.euler_deg.c); ~~~ This endpoint depends on both **/joint_states** and MoveIt. If either is unavailable, it returns 503. ### Named positions GET **/robot/positions** reads `group_state` positions from the SRDF. The list is not hardcoded in the API; it reflects the current robot configuration. The standard configuration includes **home**, **work**, and **transport**. === "curl" ~~~ bash curl -sS $HOST/robot/positions | python3 -m json.tool ~~~ === "Python" ~~~ python response = httpx.get(f"{HOST}/robot/positions", timeout=T_READ) response.raise_for_status() for position in response.json(): print(position["name"], "—", position["description"]) ~~~ === "MATLAB" ~~~ matlab namedPositions = webread([HOST '/robot/positions'], readOpts); for i = 1:numel(namedPositions) fprintf('%s — %s\n', namedPositions(i).name, ... namedPositions(i).description); end ~~~ Call this endpoint before **/robot/move/named** to obtain the exact name, planning group, and target joint angles. ## Motion commands All three commands below use MoveIt. A response has this form: ~~~ json {"success": true, "message": "Motion completed successfully"} ~~~ **success: false** means that the planner could not plan or execute the trajectory. The HTTP status may still be 200, so application code must check both the HTTP status and the **success** field. ### Moving to a named position POST **/robot/move/named** moves the manipulator to a position from the SRDF. | Field | Required | Value | |---|---:|---| | name | yes | Position name from /robot/positions | | speed | no | Speed scale from 0.01 to 1.0; default: 0.1 | | accel_scale | no | Acceleration scale from 0 to 1.0; 0 uses speed | === "curl" ~~~ bash curl -sS --max-time 60 -X POST $HOST/robot/move/named \ -H "Content-Type: application/json" \ -d '{"name": "home", "speed": 0.1, "accel_scale": 0.0}' ~~~ === "Python" ~~~ python response = httpx.post( f"{HOST}/robot/move/named", json={"name": "home", "speed": 0.1, "accel_scale": 0.0}, timeout=T_MOVE, ) response.raise_for_status() result = response.json() if not result["success"]: raise RuntimeError(result["message"]) ~~~ === "MATLAB" ~~~ matlab body = struct('name', 'home', 'speed', 0.1, 'accel_scale', 0.0); reply = webwrite([HOST '/robot/move/named'], body, moveOpts); assert(reply.success, reply.message) ~~~ ### Cartesian TCP motion POST **/robot/move/pose** accepts a TCP position in meters and an ABC orientation in radians. If **frame_id** is empty, the frame from the planning settings is used; in the standard configuration this is **base_link**. | Field | Required | Value | |---|---:|---| | x, y, z | yes | TCP coordinates, m | | a, b, c | no | KUKA ABC angles, rad; default: 0 | | speed | no | Speed scale from 0.01 to 1.0; default: 0.1 | | planner | no | ompl, ptp, lin, circ, or chomp; default: ptp | | frame_id | no | Target pose frame; an empty string uses the default frame | The **planner** value is converted to lowercase. PTP is suitable for transitions between points; LIN produces straight-line tool motion. CIRC is appropriate only when it is supported by the planner and target pose. === "curl" ~~~ bash curl -sS --max-time 60 -X POST $HOST/robot/move/pose \ -H "Content-Type: application/json" \ -d '{ "x": 0.40, "y": 0.00, "z": 0.50, "a": 0.0, "b": 3.14159, "c": 0.0, "speed": 0.1, "planner": "ptp", "frame_id": "" }' ~~~ === "Python" ~~~ python target = { "x": 0.40, "y": 0.00, "z": 0.50, "a": 0.0, "b": 3.14159, "c": 0.0, "speed": 0.1, "planner": "ptp", "frame_id": "", } response = httpx.post(f"{HOST}/robot/move/pose", json=target, timeout=T_MOVE) response.raise_for_status() print(response.json()) ~~~ === "MATLAB" ~~~ matlab body = struct( ... 'x', 0.40, 'y', 0.00, 'z', 0.50, ... 'a', 0.0, 'b', pi, 'c', 0.0, ... 'speed', 0.1, 'planner', 'ptp', 'frame_id', ''); reply = webwrite([HOST '/robot/move/pose'], body, moveOpts); assert(reply.success, reply.message) ~~~ ### Moving by joint angles POST **/robot/move/joints** accepts exactly seven angles in J1–J7 order. The API validates the number of values and the current limits from **joint_limits.yaml**. | Joint | Allowed angle, rad | |---|---:| | J1 | -2.97 to 2.97 | | J2 | -2.10 to 2.10 | | J3 | -2.97 to 2.97 | | J4 | -2.10 to 2.10 | | J5 | -2.97 to 2.97 | | J6 | -2.10 to 2.10 | | J7 | -3.05 to 3.05 | If the limits file changes, use Swagger UI as the reference. The table above describes the supplied configuration. === "curl" ~~~ bash curl -sS --max-time 60 -X POST $HOST/robot/move/joints \ -H "Content-Type: application/json" \ -d '{"joints": [0.0, 0.5, 0.0, -1.57, 0.0, 1.57, 0.0], "speed": 0.1}' ~~~ === "Python" ~~~ python response = httpx.post( f"{HOST}/robot/move/joints", json={ "joints": [0.0, 0.5, 0.0, -1.57, 0.0, 1.57, 0.0], "speed": 0.1, }, timeout=T_MOVE, ) response.raise_for_status() print(response.json()) ~~~ === "MATLAB" ~~~ matlab body = struct( ... 'joints', [0.0, 0.5, 0.0, -1.57, 0.0, 1.57, 0.0], ... 'speed', 0.1); reply = webwrite([HOST '/robot/move/joints'], body, moveOpts); assert(reply.success, reply.message) ~~~ ## Joint trajectories Endpoints under **/trajectory** publish a `JointTrajectory` message directly to **/iiwa_arm_controller/joint_trajectory**. A **status: sent** response confirms publication, not completion of motion or absence of controller errors. Monitor **/robot/joint_states** and inspect **/trajectory/logs** when necessary. ### JSON trajectory POST **/trajectory/send** accepts one or more points. | Field | Value | |---|---| | points | Non-empty point list | | points[].positions | Exactly 7 J1–J7 angles in radians | | points[].time_from_start | Time from trajectory start in seconds, at least 0 | | validate_limits | Validate joint limits; default: true | The server does not check that time increases between points. Set increasing values yourself to make controller behavior predictable. === "curl" ~~~ bash curl -sS --max-time 20 -X POST $HOST/trajectory/send \ -H "Content-Type: application/json" \ -d '{ "points": [ {"positions": [0, 0, 0, 0, 0, 0, 0], "time_from_start": 0.0}, {"positions": [0, 0.5, 0, -1.0, 0, 1.0, 0], "time_from_start": 3.0}, {"positions": [0, 0, 0, 0, 0, 0, 0], "time_from_start": 6.0} ], "validate_limits": true }' ~~~ === "Python" ~~~ python trajectory = { "points": [ {"positions": [0.0] * 7, "time_from_start": 0.0}, {"positions": [0.0, 0.5, 0.0, -1.0, 0.0, 1.0, 0.0], "time_from_start": 3.0}, {"positions": [0.0] * 7, "time_from_start": 6.0}, ], "validate_limits": True, } response = httpx.post(f"{HOST}/trajectory/send", json=trajectory, timeout=T_READ) response.raise_for_status() print(response.json()) ~~~ === "MATLAB" ~~~ matlab p1 = struct('positions', [0, 0, 0, 0, 0, 0, 0], ... 'time_from_start', 0.0); p2 = struct('positions', [0, 0.5, 0, -1.0, 0, 1.0, 0], ... 'time_from_start', 3.0); trajectory.points = [p1, p2]; trajectory.validate_limits = true; opts = weboptions('MediaType', 'application/json', 'Timeout', T_READ); reply = webwrite([HOST '/trajectory/send'], trajectory, opts); disp(reply) ~~~ ### Uploading CSV POST **/trajectory/send_csv** accepts a CSV file in the multipart **file** field. The first row must be a header. Joint columns may be named **joint1** or **joint_1**, case-insensitively. The time column may be named **t**, **time**, or **time_from_start**. Columns may appear in any order. Example file: ~~~ csv joint1,joint2,joint3,joint4,joint5,joint6,joint7,t 0,0,0,0,0,0,0,0.0 0,0.5,0,-1.0,0,1.0,0,3.0 ~~~ Pass **separator** and **validate_limits** in the query string, not as form fields. The default separator is a comma and limit validation is enabled. === "curl" ~~~ bash curl -sS --max-time 20 -X POST \ "$HOST/trajectory/send_csv?separator=%2C&validate_limits=true" \ -F "file=@trajectory.csv;type=text/csv" ~~~ === "Python" ~~~ python with open("trajectory.csv", "rb") as csv_file: response = httpx.post( f"{HOST}/trajectory/send_csv", params={"separator": ",", "validate_limits": True}, files={"file": ("trajectory.csv", csv_file, "text/csv")}, timeout=T_READ, ) response.raise_for_status() print(response.json()) ~~~ === "MATLAB" ~~~ matlab import matlab.net.http.* import matlab.net.http.io.* uri = URI([HOST '/trajectory/send_csv?separator=%2C&validate_limits=true']); form = MultipartFormProvider('file', FileProvider('trajectory.csv')); request = RequestMessage('post', [], form); httpOpts = HTTPOptions('ConnectTimeout', T_READ, 'ResponseTimeout', T_READ); response = request.send(uri, httpOpts); disp(response.Body.Data) ~~~ For a semicolon-delimited file, replace `%2C` with `%3B`. ### Trajectory module log GET **/trajectory/logs?n=50** returns up to 300 latest entries. The **n** parameter must be between 1 and 300. === "curl" ~~~ bash curl -sS "$HOST/trajectory/logs?n=20" | python3 -m json.tool ~~~ === "Python" ~~~ python response = httpx.get(f"{HOST}/trajectory/logs", params={"n": 20}, timeout=T_READ) response.raise_for_status() for line in response.json()["lines"]: print(line) ~~~ === "MATLAB" ~~~ matlab logs = webread([HOST '/trajectory/logs?n=20'], readOpts); disp(logs.lines) ~~~ Use the common **POST /stop** endpoint to interrupt a trajectory. The API has no **/trajectory/stop** endpoint. ## Motion sequences POST **/sequences/start** launches a separate `motion_sequence_runner` process. It reads the uploaded JSON file and sends `MoveToJoints` or `MoveToPose` targets in sequence. | Form field | Default | Purpose | |---|---:|---| | config | — | Sequence JSON file; required | | n_iterations | 3 | Number of repetitions; at least 1 | | delay_between_iterations | 5.0 | Delay between iterations, s | | bag_path | empty | rosbag output path; an empty string disables recording | | topics | empty | Comma-separated rosbag topics; empty means all discovered topics | | joints_action | cobot/move_to_joints | Action name for joint targets | | pose_action | cobot/move_to_pose | Action name for Cartesian targets | Minimal configuration: ~~~ json { "home": { "joints": [0, 0, 0, -1.57, 0, 1.57, 0], "speed": 0.1 }, "waypoints": [ { "x": 0.6, "y": 0.1, "z": 0.55, "a": 3.14, "b": 0.31, "c": 2.79, "speed": 0.2, "planner": "lin" }, { "joints": [0.5, 0.3, 0, -1.2, 0, 1.4, 0], "speed": 0.2 } ] } ~~~ A point containing **joints** is treated as a joint target. Otherwise, the runner expects Cartesian fields **x**, **y**, **z**, **a**, **b**, and **c**. ### Starting a sequence === "curl" ~~~ bash curl -sS --max-time 10 -X POST $HOST/sequences/start \ -F "config=@motion_sequence_config.json;type=application/json" \ -F "n_iterations=3" \ -F "delay_between_iterations=5.0" ~~~ === "Python" ~~~ python with open("motion_sequence_config.json", "rb") as config: response = httpx.post( f"{HOST}/sequences/start", files={"config": ("motion_sequence_config.json", config, "application/json")}, data={"n_iterations": "3", "delay_between_iterations": "5.0"}, timeout=T_READ, ) response.raise_for_status() print(response.json()) ~~~ === "MATLAB" ~~~ matlab import matlab.net.http.* import matlab.net.http.io.* form = MultipartFormProvider( ... 'config', FileProvider('motion_sequence_config.json'), ... 'n_iterations', '3', ... 'delay_between_iterations', '5.0'); request = RequestMessage('post', [], form); httpOpts = HTTPOptions('ConnectTimeout', T_READ, 'ResponseTimeout', T_READ); response = request.send(URI([HOST '/sequences/start']), httpOpts); disp(response.Body.Data) ~~~ A **status: started** response confirms that the process started, not that the JSON is valid or that every motion succeeds. If the runner exits with an error, inspect its status and log. ### Sequence status and log === "curl" ~~~ bash curl -sS $HOST/sequences/status | python3 -m json.tool curl -sS "$HOST/sequences/logs?n=50" | python3 -m json.tool ~~~ === "Python" ~~~ python status = httpx.get(f"{HOST}/sequences/status", timeout=T_READ) status.raise_for_status() print(status.json()) logs = httpx.get(f"{HOST}/sequences/logs", params={"n": 50}, timeout=T_READ) logs.raise_for_status() for line in logs.json()["lines"]: print(line) ~~~ === "MATLAB" ~~~ matlab status = webread([HOST '/sequences/status'], readOpts); logs = webread([HOST '/sequences/logs?n=50'], readOpts); disp(status) disp(logs.lines) ~~~ Statuses: - **idle** — no sequence has been started; - **running** — the process is running; - **finished** — the process has exited; the response includes **returncode**. Only one sequence can run at a time. A second POST to **/sequences/start** while one is running returns 409. Use **POST /stop** to stop it; there is no separate **/sequences/stop** endpoint. ## Common stop command POST **/stop** stops the running sequence runner, publishes a hold point at the current position to the trajectory controller, and calls the MoveIt **cobot/stop** service. If current joint states are unavailable, it publishes an empty trajectory instead. === "curl" ~~~ bash curl -sS --max-time 10 -X POST $HOST/stop | python3 -m json.tool ~~~ === "Python" ~~~ python response = httpx.post(f"{HOST}/stop", timeout=T_READ) response.raise_for_status() print(response.json()) ~~~ === "MATLAB" ~~~ matlab reply = webwrite([HOST '/stop'], struct(), ... weboptions('MediaType', 'application/json', 'Timeout', T_READ)); disp(reply) ~~~ This command cancels software operations, but does not remove robot power or replace the standard emergency stop. After calling it, verify both the response message and the physical robot state. ## Errors and diagnostics | Code | When it occurs | |---:|---| | 200 | The request was processed; for motion commands, also check the success field | | 409 | A motion sequence is already running | | 422 | Invalid request structure, joint count, speed, planner, or joint limits | | 503 | A ROS topic, service, action server, or MoveIt is unavailable; a wait timeout may also have occurred | When troubleshooting, proceed from simple checks to more complex ones: 1. Open **/docs** and verify that the server is running and the endpoint appears in the schema. 2. Check **/robot/joint_states**. Without it, pose retrieval does not work and trajectory stopping cannot generate a hold point. 3. Make sure that the complete stack is running: `controller_manager`, MoveIt, and `iiwa_motion_server`. 4. After starting a sequence, inspect **/sequences/logs**. After publishing a trajectory, inspect **/trajectory/logs**. The MCP server runs in the same process but provides a separate interface at **http://server-address:8007/mcp/mcp**. It uses the same Bearer token; pass the `Authorization` header when connecting an MCP client. For ordinary HTTP integrations, use the endpoints documented on this page.