3 API

All APIs should be treated as Beta software. While software that is controlling the robot through the API is being developed the robot should be safety guarded as a mistake in the software being developed can result in unexpected motion.

3.1 Python SDK

The Python SDK lets you control and communicate with the robot using your own Python code. It gives you access to tools and commands for things like moving the robot, reading sensor data, and connecting to other systems like cameras or cloud services. This is good for users who want more flexibility, whether you’re building custom applications, automating tasks, or adding new features beyond what the built-in interface can do.

You can access the Python SDK by going to API Docs | Standard Bots and selecting Python SDK.

Once there you will be brought to the SDK configure page where it will guide you through installing the SDK and how to create a connection.

The sections that follow, up to the REST API section, cover connecting, reading state, moving the arm, running saved routines, driving the gripper, and reading and writing I/O. No ROS2 is required for any of it.

3.2 Installing and Authenticating

Install the package from PyPI:

pip install standardbots

To get a token, open Configure Developer API under your robot’s name in the robot UI, enable the API, and copy the Authorization Token. The token is specific to that robot.

Your robot URL is one of the following:

3.3 Connecting to the Robot

from standardbots import StandardBotsRobot, models

ROBOT_URL = "https://cb1234.sb.app"   # or http://<robot-ip>:3000
ROBOT_TOKEN = "your-api-token"        # from Configure Developer API

sdk = StandardBotsRobot(
    url=ROBOT_URL,
    token=ROBOT_TOKEN,
    # .Simulated hits the built-in sim; .Live drives the real arm
    robot_kind=StandardBotsRobot.RobotKind.Simulated,
)

# The context manager opens and cleans up the connection for you.
with sdk.connection():
    health = sdk.status.health.get_health().ok()
    print(health.health)   # 'ok' | 'warning' | 'error'

WARNING – Validate in Simulation First

The robot_kind argument decides whether your commands hit the simulator or the real arm. Develop and test against RobotKind.Simulated, and check the flag before switching to RobotKind.Live. Every robot runs a built-in simulator, so there is no reason to debug new motion code on live hardware.

3.4 How Every Call Works

Every endpoint returns a Response object that wraps the data, the HTTP status, and the raw urllib3 response. You can inspect it directly, or unwrap it with ok(), which asserts that the status is exactly 200 and returns the payload, raising otherwise:

# Inspect the raw response
response = sdk.movement.brakes.get_brakes_state()
if response.status == 200:
    print(response.data)
else:
    raise Exception("Bad response!")

# Or, more simply, unwrap with ok()
brakes = sdk.movement.brakes.get_brakes_state().ok()

Request and response bodies are built from models. Two of them come up constantly:

position = models.Position(
    # Meters | Millimeters | Centimeters | Inches | Feet
    unit_kind=models.LinearUnitKind.Meters,
    x=0.4, y=0.0, z=0.3,
)
orientation = models.Orientation(
    kind=models.OrientationKindEnum.Quaternion,
    quaternion=models.Quaternion(x=0.0, y=1.0, z=0.0, w=0.0),
)

Scalar numeric model fields are type-checked at runtime and must be floats. Passing an integer such as z=0 raises TypeError. See the gotchas below.

3.5 Control Surface Reference

Everything hangs off the sdk object. The groups you will use most:

SDK group What it does Key calls
sdk.status Health and control mode health.get_health(), control.get_configuration_state_control()
sdk.movement.brakes Release and engage brakes, E-stop unbrake(), brake(), get_brakes_state()
sdk.movement.position Move the arm and read its position move_tooltip(), move(), get_arm_position()
sdk.poses.pose_retrieval Read TCP, joint, and flange pose get_tooltip_position(), get_joints_position(), get_flange_position()
sdk.routine_editor.routines Run saved routines list(), load(), play(), pause(), stop(), get_state()
sdk.routine_editor.variables Read and write routine variables load(), update()
sdk.equipment Gripper and end-effector get_gripper_configuration(), onrobot_2fg7_grip(), control_gripper()
sdk.io Digital and analog I/O status.get_io_state(), control.update_io_state()
sdk.recovery Inspect and clear faults recover.get_status(), recover.recover()
sdk.payload Set and read payload mass set_payload(), get_payload()
sdk.camera Wrist camera frames bot.get_connected_cameras(), data.get_camera_frame()

These map one-to-one onto the REST routes described in the REST API section below.

3.6 The Pattern for Any Motion

Direct arm-motion calls require the arm to be unbraked and the robot to accept API control. The skeleton is take control, unbrake, move, then release:

with sdk.connection():
    # (a) If the robot is in routine-editor control, hand control to the API.
    sdk.status.control.set_configuration_control_state(
        models.RobotControlMode(kind=models.RobotControlModeEnum.Api)
    ).ok()

    # (b) Release the brakes.
    sdk.movement.brakes.unbrake().ok()

    # (c) ... do your motion (see the use cases below) ...

    # (d) Optionally re-engage the brakes when you are done.
    sdk.movement.brakes.brake().ok()

Running a saved routine is itself a control action and does not require you to manage the brakes, because the routine handles its own motion. Steps (a) and (b) are only for direct move_tooltip and move commands.

3.7 Use Case A – Reading Live Robot State

This answers where the arm is and whether it is healthy. It is read-only, so no unbrake step is needed.

with sdk.connection():
    # TCP pose: position (x, y, z) + orientation quaternion (i, j, k, w)
    pose = sdk.poses.pose_retrieval.get_tooltip_position().ok().pose
    print("xyz :", pose.x, pose.y, pose.z)
    print("quat:", pose.i, pose.j, pose.k, pose.w)

    # Per-joint angles j0 through j5
    joints = sdk.poses.pose_retrieval.get_joints_position().ok().pose
    print("joints:", joints.j0, joints.j1, joints.j2,
          joints.j3, joints.j4, joints.j5)

    # Health rollup
    print("health:", sdk.status.health.get_health().ok().health)

NOTE – Two Different Quaternion Field Names

The TCP pose you read back stores orientation in i, j, k, w, while the models.Quaternion you pass into a move uses x, y, z, w. The mapping is i to x, j to y, k to z, and w to w. Use case B shows the conversion.

get_arm_position() is a one-call alternative that returns joint angles and the TCP pose together:

with sdk.connection():
    combined = sdk.movement.position.get_arm_position().ok()
    print("joints:", combined.joint_rotations)   # (j0 ... j5)
    tcp = combined.tooltip_position              # PositionAndOrientation
    print("tcp xyz:", tcp.position.x, tcp.position.y, tcp.position.z)

3.8 Use Case B – Moving the Arm to a Pose

The safest pattern is to read the current pose and then command a small offset from it, so that you never have to guess an absolute orientation.

with sdk.connection():
    sdk.movement.brakes.unbrake().ok()

    # Current TCP pose
    pose = sdk.poses.pose_retrieval.get_tooltip_position().ok().pose

    # Move 5 cm straight up, keeping the same orientation
    sdk.movement.position.move_tooltip(
        position=models.Position(
            unit_kind=models.LinearUnitKind.Meters,
            x=pose.x, y=pose.y, z=pose.z + 0.05,
        ),
        orientation=models.Orientation(
            kind=models.OrientationKindEnum.Quaternion,
            quaternion=models.Quaternion(
                x=pose.i, y=pose.j, z=pose.k, w=pose.w,
            ),
        ),
        # .Line = straight-line TCP path; .Joint = fastest joint-space path
        movement_kind=models.MovementKindEnum.Line,
    ).ok()

Things to notice in this sample:

To command the six joints directly instead of a Cartesian target, for example to reach a known configuration, use set_arm_position with a joint rotation request:

with sdk.connection():
    sdk.movement.brakes.unbrake().ok()

    sdk.movement.position.set_arm_position(models.ArmPositionUpdateRequest(
        kind=models.ArmPositionUpdateRequestKindEnum.JointRotation,
        joint_rotation=models.ArmJointRotations(
            joints=(0.0, -1.2, 1.2, 0.0, 1.57, 0.0),
        ),
        movement_kind=models.MovementKindEnum.Joint,
    )).ok()

Joints are a six-tuple of j0 through j5 in the robot’s joint-angle convention, in radians. Check your joint limits in the API docs before sending absolute targets. set_arm_position also accepts kind=...TooltipPosition, which is exactly what move_tooltip builds for you.

3.9 Use Case C – Running a Saved Routine

This triggers a routine you built on the pendant or web app, optionally overriding its variables, and then watches it run.

with sdk.connection():
    # Find the routine you want
    routines = sdk.routine_editor.routines.list(limit=100, offset=0).ok()
    for r in routines.items:
        print(r.id, r.name)

    routine_id = "routine_xxxxxxxxxxxx"

    # Start it. Always pass a PlayRoutineRequest body; variables optional.
    sdk.routine_editor.routines.play(
        body=models.PlayRoutineRequest(variables={"cycle_count": "5"}),
        routine_id=routine_id,
    ).ok()

    # Monitor progress
    state = sdk.routine_editor.routines.get_state(routine_id=routine_id).ok()
    print("paused:", state.is_paused, "step:", state.current_step_id,
          "cycles:", state.cycle_count)

    # Pause and stop
    sdk.routine_editor.routines.pause(routine_id=routine_id).ok()
    sdk.routine_editor.routines.stop().ok()

WARNING – Always Pass a Body to play()

Always pass body=models.PlayRoutineRequest(...) to play(). Older SDK versions behaved badly when it was omitted, including running live instead of simulated. Passing the body explicitly and keeping the SDK current avoids this.

You can also read and set routine variables out of band with sdk.routine_editor.variables.load(variable_name=...) and .update(body=models.RuntimeVariable(value="..."), variable_name=...).

3.10 Use Case D – Controlling the Gripper

Confirm what is attached first, then drive it. The OnRobot 2FG7 has convenience helpers:

with sdk.connection():
    cfg = sdk.equipment.get_gripper_configuration().ok()
    print("gripper:", cfg.kind)   # e.g. GripperKindEnum.Onrobot2Fg7

    # Position move to a 50 mm opening
    sdk.equipment.onrobot_2fg7_move(
        value=50, unit_kind=models.LinearUnitKind.Millimeters,
    ).ok()

    # Force grip inward to 20 mm at 40 N
    sdk.equipment.onrobot_2fg7_grip(
        value=20,
        direction=models.LinearGripDirectionEnum.Internal,
        force=40, force_unit=models.ForceUnitKind.Newtons,
    ).ok()

The generic call works for any supported gripper, covering the OnRobot 2FG7, 2FG14, 3FG15, and Screwdriver, the Schunk EGx, and the Robotiq 2F and EPick:

sdk.equipment.control_gripper(models.GripperCommandRequest(
    kind=models.GripperKindEnum.Onrobot2Fg7,
    onrobot_2fg7=models.OnRobot2FG7GripperCommandRequest(
        grip_direction=models.LinearGripDirectionEnum.Internal,   # required
        control_kind=models.OnRobot2FG7ControlKindEnum.Move,
        target_grip_width=models.LinearUnit(
            unit_kind=models.LinearUnitKind.Millimeters,
            value=50.0,   # must be a float, not 50
        ),
    ),
)).ok()

If a force grip fails, confirm that a gripper is actually configured and powered. get_gripper_configuration().kind should not be NoneConnected. Force-close errors usually trace back to an unconfigured or unpowered end-effector rather than to your code.

3.11 Use Case E – Reading and Writing I/O

Read first. The response shows you the exact pin keys and value strings your robot uses, so you never have to guess them.

with sdk.connection():
    io = sdk.io.status.get_io_state().ok()
    # e.g. {"tool_digital_out_0": "high", "base_digital_in_2": "low"}
    print(io.state)

    # Write: send back the same shape with only the pins you want changed
    sdk.io.control.update_io_state(
        models.IOStateUpdateRequest(state={"tool_digital_out_0": "high"})
    ).ok()

If I/O writes do not seem to take effect, confirm that the controller software is current. On older builds, an I/O step inside a routine is a workaround.

3.12 Use Case F – Recovering from a Fault or E-Stop

with sdk.connection():
    state = sdk.recovery.recover.get_status().ok()
    # RobotStatusEnum: Idle | RoutineRunning | Failure | Recovering | ...
    print("status:", state.status)
    print("failed:", state.failed)
    if state.failure:
        print("kind         :", state.failure.kind)
        print("reason       :", state.failure.reason)
        print("recovery_type:", state.failure.recovery_type)

    # Clear a software-recoverable fault
    if state.failed:
        sdk.recovery.recover.recover().ok()

WARNING – Not Every Fault Clears from Software

The recovery_type field tells you what is needed, which may be a guided-mode or manual recovery, a restart, or a physical wrist-button press. A physical E-stop must be released by hand before recover() will succeed.

3.13 Use Case G – Using a Saved Space

Spaces are the named positions you save in the app. This drives the arm to a position you taught on the pendant.

with sdk.connection():
    spaces = sdk.space.list_global_spaces().ok()
    for s in spaces.items:
        print(s.id, s.name)   # find the Space you want

A Space carries its saved coordinates in s.positions, where each entry has joint angles, a Cartesian pose, or both. To move there, feed those values into a move_tooltip or set_arm_position call as shown in use case B, or run a routine that already references the Space. Routine-scoped, non-global Spaces are available through sdk.routine_editor.routines.list_spaces(routine_id=..., exclude_global_spaces=...).

3.14 Use Case H – Setting the Payload

Tell the robot the mass it is carrying so that motion planning stays accurate. Set it after a pick and clear it after a place.

with sdk.connection():
    sdk.payload.set_payload(models.PayloadStateRequest(mass=2.0)).ok()   # kg
    print("payload:", sdk.payload.get_payload().ok().mass)

3.15 Use Case I – Triggering a Software Emergency Stop

You can command an E-stop from code, for example when a watchdog trips, and then recover afterward using the flow in use case F. This is distinct from the physical button, which must still be released by hand.

with sdk.connection():
    sdk.movement.brakes.engage_emergency_stop(
        models.EngageEmergencyStopRequest(reason="watchdog tripped")
    ).ok()

3.16 Troubleshooting

3.17 Known Limitations

3.18 REST API

With REST API you will be able to control the movements of your robot. Leverage reference frames, tooltips, and more to accomplish your task.

You can access the REST API by going to API Docs | Standard Bots and selecting Move Your Robot

You will then be brought to the Arm Movement and Position Control Page. Here it will walk you through moving the robot via the API.

The Python SDK is a typed wrapper over these same REST routes, so anything the SDK does is also available over plain HTTP. Use the REST API directly when you are working in a language other than Python, calling the robot from a PLC or an existing HTTP client, or debugging with a tool such as curl.

3.18.1 Authentication and Base URL

Authentication uses the same per-robot Authorization Token described in the Python SDK section, copied from Configure Developer API in the robot UI. The base URL forms are also the same: https://cb1234.sb.app for the cloud address, http://<robot-ip>:3000 on the LAN, or http://localhost:3000 when running on the controller itself. Routes are served under /api/v1/.

3.18.2 Route Map

Each SDK group corresponds directly to a REST route, so the SDK is a reliable guide to the HTTP surface:

SDK group REST route
sdk.movement.position /api/v1/movement/position
sdk.movement.brakes /api/v1/movement/brakes
sdk.poses.pose_retrieval /api/v1/poses
sdk.routine_editor.routines /api/v1/routine-editor/routines
sdk.status.control /api/v1/status/control
sdk.equipment /api/v1/equipment
sdk.io /api/v1/io
sdk.recovery /api/v1/recovery

The ROS2 bridge control state used later in this chapter is also a REST route: GET and POST /api/v1/movement/ros/state.

For the exact request and response bodies, query parameters, and the full route list, use the API reference at docs.standardbots.com. The units, enum values, float-versus-integer rules, and quaternion field names documented earlier in this chapter apply to the REST payloads as well.