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.
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.
Install the package from PyPI:
pip install standardbotsTo 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:
https://cb1234.sb.app — the cloud address for your
controller, with your own serial number in place of
cb1234.http://<robot-ip>:3000 — direct on the LAN.http://localhost:3000 — when running on the controller
itself.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.
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.
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.
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.
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)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:
move_tooltip moves the tool center
point to the target. move is a thin alias that
calls move_tooltip with the same arguments, so both perform
a tooltip move. Choose the frame the target is expressed in with
axis_alignment or reference_frame, for example
axis_alignment="tooltip" versus "base", rather
than by picking one method over the other.movement_kind is either
Line, a straight TCP path, or Joint, the
fastest joint-space path, which is curved in Cartesian space.speed_profile=models.SpeedProfile(max_tooltip_speed=0.25).
Start slow when validating new targets.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.
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=...).
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.
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.
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.
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 wantA 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=...).
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)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()connection refused against a real
robot. Confirm that the Developer API is enabled, that the host
is reachable from your machine, and that you are using the right URL
form. Intermittent connection refused after a few good
calls has been seen on older controller software, so update the
controller if you hit it.robot_kind controls this. Develop on
Simulated, verify the flag before going Live,
and keep the SDK current.unbrake(); or there
is an active fault, so see use case F; or the robot is in routine-editor
control or already running a routine. Check
sdk.status.control.get_configuration_state_control() and
switch to Api.get_io_state() first to discover the exact keys and
values.standardbots==<version> and
re-test on upgrade.Position.x, .y, and .z,
Quaternion, LinearUnit.value,
ForceUnit.value,
SpeedProfile.max_tooltip_speed, and
PayloadStateRequest.mass are type-checked at runtime.
Passing an integer such as z=0 or value=50
raises TypeError: ... must be of type float. Write
0.0 and 50.0 instead. Tuple fields such as
ArmJointRotations.joints do accept integers.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.
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/.
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.