Quick start
Start with public, read-only requests. These do not join a queue or move a robot. No BluPe credential or model API key is required.
export BLUPE_API="https://yam-session-api.n5hthc3gj4cqy.us-east-1.cs.amazonlightsail.com"
curl --fail-with-body "$BLUPE_API/v1/robots"
curl --fail-with-body "$BLUPE_API/v1/robots/yam-1/queue"
curl --fail-with-body "$BLUPE_API/v1/robots/yam-1/observation"The queue reports station connection and availability. An empty queue or a
successful /health response does not mean the robot is ready. The observation
contains measured joint state, readiness/safety fields, timestamps, and camera
URLs. Fetch the returned image URLs to see the scene.
Base URL: https://yam-session-api.n5hthc3gj4cqy.us-east-1.cs.amazonlightsail.com
The live v1 JSON schemas define request and response fields. This endpoint returns JSON Schema, rather than an OpenAPI/Swagger interface. Deployment-specific bounds can change; check that contract when building a client. Examples below target ordinary v1 sessions. The public read contract was checked on October 25, 2026.
Choose a robot
Use GET /v1/robots for registered IDs, then read that robot's queue and
observation. Registration does not establish availability. Discovery includes
calibration links when configured; it does not currently include complete joint
layouts, joint limits, or a machine-readable motion capability profile.
The current connected stations are the two YAM rigs below, checked on October 25, 2026. Use one of these IDs for the examples in this guide.
| Robot | Robot ID | Left / right arm joints | Camera roles |
|---|---|---|---|
| YAM | yam-1 |
6 / 6 | left, top, right |
| Robo-house YAM | robot-ba8413962083809c |
6 / 6 | left, top, right |
Andrew's single SO101 and MakerMods Bimanual SO101 are currently offline and are not included as available stations. Their IDs can still appear in discovery. A registered ID or bundled runner profile does not mean that hardware is online.
Connected does not mean ready for motion. Both YAM rigs reported
available: false and mode: "DISABLED" at this check. Before requesting a turn,
read the selected robot's live queue and observation; follow its readiness and
session feedback rather than treating this table as a live status display.
Both current rigs use six joint angles per arm, in degrees, with independent grippers. Their station geometry and calibrated limits differ; use the profile for the selected robot. Never mirror or pad an arm.
See robot selection and profiles and the camera calibration below. Cartesian frames, workspace bounds, calibration, and hardware execution checks must match the selected robot.
The shared runner also contains SO101 and MakerArm integration profiles
for separately configured hardware. Those profiles describe client support,
not additional currently connected stations. A single-arm integration sends the
absent arm as [] and omits its gripper.
Authentication
Public monitoring and session creation require no BluPe account credential. Creating a session returns an opaque session capability for that session's HTTP requests and event WebSocket:
Authorization: Bearer <session_capability>
Content-Type: application/jsonKeep the capability in your backend process. Do not put it in URLs, page source, source control, or logs. It is returned at creation, not by later status reads. The Python client stores and attaches it for you.
A lease ID identifies the current grant of robot control. It is distinct from the capability, and accompanies motion requests together with the episode ID. Queued sessions may not have an episode or lease yet.
Your model-provider API key or subscription login authenticates inference with your provider. Keep it separate from BluPe requests. Your policy performs inference and inverse kinematics in your own process or service; ordinary trajectory requests contain motion data, not provider credentials or weights.
Session lifecycle
Your program joins the selected queue, waits for control and fresh feedback, then repeats observe → infer → submit → wait for execution. When finished, it sends Stop to release the session.
Create a session
POST /v1/sessions creates a real queue entry. Example request body:
{
"schema_version": 1,
"robot_id": "yam-1",
"prompt": "Place the green block on the plate",
"run_duration_s": 300
}Ordinary sessions default to 300 seconds, with a supported range of
60–600 seconds. The live schema also describes conditional extended durations
for provider: "dream" or "dream_flash"; those are provider-specific sessions,
not a way to extend an ordinary trajectory run. Use the duration and conditions
supported by the selected runner and deployment.
Optional creation fields include runner_name, email, email_run_video, and
public_conversation. A video-email request requires an email address; delivery
also depends on the runner/hosted integration. See video email behavior.
HTTP 201 returns session_id, session_capability, status, position,
episode_id, lease_id, lease_expires_at, latest_observation_step, and
events_url. Episode/lease fields may be null while queued. Save the creation
response privately. Session creation has no idempotency key: do not automatically
create another session after an ambiguous response.
Wait for control
Open the returned event path on the same API host, using wss:// and the Bearer
header in the WebSocket handshake. The Python open_events() method demonstrates
this. Browser WebSockets cannot set that header; use a backend client.
A normal lifecycle is queued → preparing → running → complete → resetting.
Other terminal outcomes include stopped, timed_out, disconnected, and
safety_aborted. Treat resetting as terminal for the client. Being queued or
receiving a lease alone does not establish physical readiness. Wait for fresh
measured, homed/settled hardware feedback and inspect safety/readiness fields.
Events carry schema_version, event_id, type, timestamp, session_id,
episode_id, and payload:
| Event type | What to track |
|---|---|
snapshot, queue_update |
Status, queue position, episode, lease, and active trajectory |
lifecycle |
payload.state and transition reason |
observation |
Measured joints, grippers, timestamp, and images in payload |
trajectory_result |
Correlated packet accepted, rejected, completed, or aborted |
trajectory_progress |
Executed waypoint step_id for a trajectory |
heartbeat |
Gateway lease renewal; this is a server-to-client event |
episode, error |
Results or structured failures |
A new subscription starts with a snapshot and may replay the newest observation
and trajectory result (payload.replayed: true). It is not a complete event
history. Match episode, lease, trajectory, and step IDs; replayed state must not
trigger duplicate motion. Use observation pagination or the episode trace for
retained history. The next observation follows the last executed waypoint's ID.
The station gateway renews the lease; there is no client REST heartbeat route. A closed WebSocket does not itself stop the session. On a connection failure, pause command generation, reconnect to the same session, and reconcile its snapshot/trace before continuing. Request Stop if you cannot recover safely.
Send a trajectory
Send POST /v1/sessions/{session_id}/trajectories only after your session has
control and its previous packet has finished. The following illustrates the
wire format only. Its zero angles are placeholders, not a hardware motion
recipe. Compute targets from fresh feedback and the selected robot's profile.
{
"schema_version": 1,
"episode_id": "ep_example",
"lease_id": "lease_example",
"trajectory_id": "trajectory_example_0",
"cadence_hz": 10.0,
"waypoints": [
{
"step_id": 0,
"left_joints_deg": [0, 0, 0, 0, 0, 0],
"right_joints_deg": [0, 0, 0, 0, 0, 0],
"left_gripper": 0.5,
"right_gripper": 0.5
}
]
}- Use finite degree values and the exact arm lengths for your robot. Independent
grippers are optional, from
0(closed) to1(open). - Cadence is exactly
10.0Hz. Each packet contains 1–300 waypoints. - Start waypoint IDs at
0, keep them contiguous within a packet, and continue across packets. Only one packet may be active at a time. - Give new packets unique trajectory IDs. Retrying requires the same ID and identical body. Changed content under an existing ID causes a conflict.
- The shared runner caps a session at 3,000 accepted waypoints. Server budgets can differ by deployment/session type. The live schema currently allows a wider step-ID range; that field range alone is not a guaranteed motion budget.
- Planning, inference, execution, and settling consume the run duration. A waypoint budget does not extend the deadline. Gateway limits still apply.
HTTP 202 with accepted: true means dispatched, not physically executed.
Follow the matching gateway acceptance and ordered progress, its final settled
measured observation, and completed result before planning another packet.
A rejection or abort requires reconciliation; an aborted packet may have moved
through some waypoints. Do not treat network failure as proof that no motion
occurred. The bundled controller performs these correlations and reconnect
recovery, including trace checks when progress events were missed.
The public wire API accepts joint trajectories. Cartesian targets and model tool
calls in the runner are converted locally; they are not separate public motion
endpoints. The old /actions route is rejected.
Stop and retrieve results
POST /v1/sessions/{session_id}/stop, using the session capability and JSON:
{"reason": "user_requested"}Use policy_complete when your policy has finished. Stop also cancels a queued
session. A completed packet retains the session for your next model call; send
Stop when the task ends. There is no public /done route. Model-reported success
is not independent verification that the physical task succeeded.
A Stop response reports API session state; it is not independent confirmation of physical Hold or a promise about a return-home path. Follow measured station feedback. The gateway controls physical stopping/reset behavior.
Read /episode for metadata and /episode/trace for actions, measured
observations, progress/results, errors, and lifecycle. Observation pages accept
after_step_id (initially -1) and limit (1–100), and return has_more and
next_after_step_id. Trace retention is configurable, one hour after completion
by default in the server implementation. Check available_until and download
needed results promptly; an expired trace returns 410.
API traces are separate from model-conversation logs and recorded videos. The runner saves local logs/recordings. Published recordings appear in Public-YAM-runs and Public-MakerMods-SO101-runs where recording/upload is configured. A robot registration does not guarantee recording or immediate video availability.
Camera intrinsics and extrinsics
Camera calibration is available through public, read-only requests. No API key,
queue entry, or active session is required. Start with GET /v1/robots: robots
with calibration advertise calibration_url and camera_poses_url, relative to
the Session API origin. A robot without configured calibration returns 404.
export BLUPE_API="https://yam-session-api.n5hthc3gj4cqy.us-east-1.cs.amazonlightsail.com"
curl --fail-with-body "$BLUPE_API/v1/robots/yam-1/calibration"
curl --fail-with-body "$BLUPE_API/v1/robots/yam-1/camera-poses"Which parameters to use
The /calibration response has a calibration_id and a cameras object:
| Field | Meaning |
|---|---|
cameras.{top,left,right}.K |
3×3 intrinsic matrix in pixels: [[fx,0,cx],[0,fy,cy],[0,0,1]] |
cameras.{top,left,right}.distortion |
Lens coefficients; read distortion_model and the response's distortion_order |
cameras.{top,left,right}.image_size |
[width, height] for these intrinsics; use the matching image resolution |
cameras.top.T_base_camera.left |
Fixed top-camera pose relative to the left arm base |
cameras.top.T_base_camera.right |
Fixed top-camera pose relative to the right arm base |
cameras.left.T_grasp_camera |
Fixed left-camera mount relative to the left gripper's grasp frame |
cameras.right.T_grasp_camera |
Fixed right-camera mount relative to the right gripper's grasp frame |
cameras.{top,left,right}.image_url |
Relative URL for the corresponding RGB JPEG |
quality |
Fit status, validation residuals, failures, and operator selection |
The left and right bases are different coordinate frames. Use .left when
planning for the left arm and .right for the right arm; do not average matrices
expressed in different bases. Each T_base_camera is a 4×4 matrix mapping a
camera-coordinate point to the named arm's base:
[x_base, y_base, z_base, 1] = T_base_camera @ [x_camera, y_camera, z_camera, 1]Matrices are nested row-major arrays and act on column vectors. Translations
are in meters. Camera optical axes are x right, y down, z forward;
arm-base axes are x forward, y left, z up. Read the returned
transform_convention, camera_axes, and base_axes with each calibration.
Read the returned distortion model. YAM packages support OpenCV Brown-Conrady
(opencv_brown_conrady, coefficients [k1, k2, p1, p2, k3]) and RealSense
factory profiles (realsense_factory, with depth_intrinsics). Account for distortion
when projecting or unprojecting pixels. Cropping or resizing an image requires
adjusting its intrinsics. Calibration maps a pixel to a ray; determining a 3D
point also requires depth, triangulation, or a known surface.
Moving wrist cameras
GET /v1/robots/yam-1/camera-poses returns the current wrist extrinsics:
cameras.left.T_base_camera.left: left wrist camera → left arm base.cameras.right.T_base_camera.right: right wrist camera → right arm base.
The API computes these from measured joints and the fixed mount:
T_base_camera = T_base_grasp(measured_joints) @ T_grasp_camera.
Refresh them after the arm moves. The response includes joints_deg,
observed_at, age_s, and the matching calibration_id; refetch calibration
if its ID changes. The static report also includes kinematics for clients
that need to reconstruct poses from recorded joint observations.
Pose requests require settled hardware feedback no more than two seconds old.
If feedback is absent, moving, stale, or simulated, the endpoint returns
503 with error code camera_pose_unavailable. The static /calibration
endpoint still works when the arms are off. A failed pose request is not a
reason to reuse an earlier wrist pose as current.
Image capture and joint feedback are not hardware synchronized:
camera-poses.synchronized_with_images is false. Its observed_at is the
joint-feedback timestamp, while camera responses provide X-Captured-At.
Check both timestamps and use settled captures for geometric measurements.
Python: retrieve calibration without moving the robot
This example uses only Python's standard library:
import json
from urllib.error import HTTPError
from urllib.request import urlopen
API = "https://yam-session-api.n5hthc3gj4cqy.us-east-1.cs.amazonlightsail.com"
ROBOT = "yam-1"
def get_json(path):
with urlopen(API + path, timeout=10) as response:
return json.load(response)
calibration = get_json(f"/v1/robots/{ROBOT}/calibration")
top = calibration["cameras"]["top"]
K = top["K"]
distortion = top["distortion"]
T_left_base_top = top["T_base_camera"]["left"]
T_right_base_top = top["T_base_camera"]["right"]
print("Calibration:", calibration["calibration_id"])
print("Top image size:", top["image_size"], "K:", K)
print("Fit quality:", calibration["quality"])
try:
poses = get_json(f"/v1/robots/{ROBOT}/camera-poses")
except HTTPError as error:
if error.code != 503:
raise
print("Current wrist poses unavailable:", error.read().decode())
else:
if poses["calibration_id"] != calibration["calibration_id"]:
raise RuntimeError("Calibration changed; fetch calibration and poses again")
T_left_base_wrist = poses["cameras"]["left"]["T_base_camera"]["left"]
T_right_base_wrist = poses["cameras"]["right"]["T_base_camera"]["right"]
print("Joint feedback timestamp:", poses["observed_at"])
print("Left wrist to left base:", T_left_base_wrist)
print("Right wrist to right base:", T_right_base_wrist)RGB-D observations
Depth availability is per camera. Read its depth object in the calibration
response. available describes fresh data now; an advertised on_demand: true
URL can request a capture even when available is false. Prefer advertised URLs
over assuming every robot/camera supports depth.
| Representation | Contents and use |
|---|---|
depth.bundle_url / .rgbd.npz |
Paired RGB-D in rgbd-npz-v1: rgb uint8 H×W×3, depth_m float32 H×W, and scalar JSON metadata |
depth.native_bundle_url / .rgbd-native.zip |
Advertised rgbd-jpeg-z16-png-v1 bundle with JPEG RGB, bit-exact sensor depth, and metadata |
depth.image_url / .depth.png |
16-bit depth PNG in millimeters; separate RGB/depth requests may select different captures |
depth.raw_bundle_url / .rgbd-raw.npz |
Original-array download when advertised; use its separate age/transfer policy |
depth.request_url / .rgbd?compression=compressed |
Negotiated compressed/native representation, when advertised; compression=none selects the raw representation |
Legacy NPZ reads use numpy.load(..., allow_pickle=False). Check camera/robot
identity, calibration_id, dimensions, and captured_at. Aligned depth measures
color-camera optical Z in meters; zero/NaN indicate missing measurements.
A surface point is not automatically a valid gripper target.
Missing or stale depth returns 204 No Content, an empty body, and
X-Camera-Error: no_image; do not try to parse it as NPZ/PNG. Retry observation
reads within your run deadline and honor cancellation. RGB JPEG reads can return
503 for missing fresh frames. Wrist-pose reads use their own 503 JSON error.
Unknown/unconfigured resources can return 404. Successful image/depth responses
carry capture timestamps; camera/calibration responses use Cache-Control: no-store.
The standard public depth freshness window is five seconds. The shared moving policy applies a tighter two-second bound unless feedback confirms the robot is stopped, and hardware policy captures must follow their settled-feedback cutoff. Different cameras are not hardware synchronized. Check source timestamps rather than substituting download time.
Inspect quality.status, quality.failures, and validation residuals before
relying on geometry. operator_selected: true records an experimental selection;
it does not turn a rejected or experimental fit into validated calibration.
See the RGB-D contract,
native bundle format, and
Astra depth example.
Endpoint reference
Paths are relative to the Session API origin. sid means your session ID.
Session HTTP and WebSocket routes require that session's Bearer capability.
| Method | Path | Access / purpose |
|---|---|---|
| GET | /health |
Public service health; not robot readiness |
| GET | /v1/contracts |
Public JSON Schema bundle |
| GET | /v1/robots |
Public registered robot IDs and calibration links |
| GET | /v1/robots/{robot_id}/queue |
Public queue and station availability |
| GET | /v1/queue?robot_id=yam-1 |
Compatibility queue route; defaults to yam-1 |
| GET | /v1/robots/{robot_id}/observation |
Public latest station feedback and camera URLs |
| WS | /v1/robots/{robot_id}/events |
Public station status stream |
| GET | /v1/robots/{robot_id}/calibration |
Public static intrinsics/extrinsics and quality |
| GET | /v1/robots/{robot_id}/camera-poses |
Public current poses from settled hardware feedback |
| GET | /v1/robots/{robot_id}/cameras/{role}.jpg |
Public latest RGB JPEG |
| GET | /v1/robots/{robot_id}/cameras/{role}.rgbd.npz |
Public paired RGB-D when configured |
| GET | /v1/robots/{robot_id}/cameras/{role}.rgbd-native.zip |
Public native bundle when advertised |
| GET | /v1/robots/{robot_id}/cameras/{role}.rgbd-raw.npz |
Public original arrays when advertised |
| GET | /v1/robots/{robot_id}/cameras/{role}.rgbd?compression=compressed |
Public negotiated bundle when advertised |
| GET | /v1/robots/{robot_id}/cameras/{role}.depth.png |
Public 16-bit depth PNG when configured |
| POST | /v1/sessions |
Public session creation; requests a real queue entry |
| GET | /v1/sessions/{sid} |
Session status, lease, and active trajectory |
| WS | /v1/sessions/{sid}/events |
Session snapshots, observations, execution events |
| GET | /v1/sessions/{sid}/observations?after_step_id=-1&limit=100 |
Paginated retained observations |
| POST | /v1/sessions/{sid}/trajectories |
Submit a joint trajectory; HTTP 202 |
| POST | /v1/sessions/{sid}/stop |
Stop or cancel your session |
| GET | /v1/sessions/{sid}/episode |
Episode metadata and retention |
| GET | /v1/sessions/{sid}/episode/trace |
Retained execution trace |
| POST | /v1/sessions/{sid}/public-conversation |
Replace or withdraw an opt-in public text snapshot |
The client retains a call_operator() helper, but the public router has no
/operator endpoint. Use Stop; the playground's Call Operator button displays
contact information. Station publisher/operator credentials and routes are
separate from this developer control API.
Errors and retries
Session JSON errors use this envelope:
{
"error": {
"code": "invalid_session_state",
"message": "a trajectory is already active for this session",
"status": 409
}
}Camera binary endpoints can return plain text or an empty body instead. Check HTTP status and content type before decoding a response.
| Status | Meaning / response |
|---|---|
| 400 | Invalid fields, shape, values, or protocol version; correct the request |
| 403 | Missing/invalid capability, or invalid/expired episode or lease context |
| 404 | Unknown session, robot, route, or unconfigured resource |
| 409 | Invalid state, active packet, sequencing, or idempotency conflict; reconcile state |
| 410 | Retained episode data expired |
| 429 | Rate limit; respect the deployment's limits before retrying |
| 502 / 503 / 504 | Contact/service problem; execution state may be uncertain |
The bundled HTTP transport retries reads and stable-ID trajectory submissions up to three total attempts, with 1- and 2-second pauses, for transient contact failures. It does not automatically retry creation or Stop. It surfaces other HTTP errors rather than blindly retrying them. The local server's default rate budget is 900 accepted waypoints per minute; deployment hooks can change it.
After an uncertain dispatch, retain the original ID/body and inspect the session and trace. Never assign a new ID just to retry the same motion. The controller reconnects the same session and verifies missed completion against recorded acceptance, ordered progress, and final feedback. A network failure does not prove that a command was rejected or that no motion occurred.
Python client and custom policies
The importable client lives in codex-runner/src/remote_yam/session.py.
From the repository root, this example performs only a public queue GET:
PYTHONPATH=codex-runner/src python3 - <<'PYTHON'
from remote_yam.session import HttpSessionAPI
api = HttpSessionAPI(
"https://yam-session-api.n5hthc3gj4cqy.us-east-1.cs.amazonlightsail.com",
robot_id="yam-1",
joint_counts=(6, 6),
supports_trajectories=True,
)
queue = api.get_queue_snapshot()
print("Queued:", len(queue["entries"]))
print([(s["connected"], s["available"]) for s in queue["stations"]])
PYTHONSet supports_trajectories=True explicitly for direct controller integration.
This is a client configuration switch, not automatic capability discovery. The
normal playground already enables it. Actual control also needs
websocket-client; the launcher installs the runner dependencies.
For a model integration, start with the shared runner
and provider interface. A provider's
infer(prompt, observation) returns a runner command for local IK; a
build_trajectory(prompt, observation, first_step_id) hook can supply waypoints.
RunnerController owns queue,
feedback, completion, retry/reconnect, and Stop handling.
Use the Python transport for an independent backend client. The ready-made UI has no arbitrary model-upload field. The existing depth example shows a concrete integration, with its own setup and qualification requirements.
Local runner API and configured execution
Runner /api/* routes belong to the process you launch, not to the public Session
API. They use a visitor cookie and X-YAM-Runner-Token CSRF value from
POST /api/session. Run/status/Stop clients must retain the same visitor.
RunnerPolicyClient
implements this for configured policies. See environment resets
for supported configuration and current physical qualification.
There is no generic public /v1/reset route. The server source also implements
lease-bound /v1/sessions/{sid}/executions and result reads for specific installed
executors. These are deployment-specific extensions, absent from the public JSON
Schema bundle; confirm the installed executor and contract before using them.
They do not accept arbitrary uploaded policy code.
Public data and conversation sharing
Robot recordings and published task information can be public. Keep private context out of tasks and opt-in shared text. Recording/upload behavior depends on the selected station and runner.
Direct API clients opt into readable model-message publication explicitly.
Omitting public_conversation does not publish a model conversation. To share
one, use the session capability with POST /v1/sessions/{sid}/public-conversation:
{
"public_conversation": {
"model": "My model",
"messages": [
{"role": "user", "content": "Stack the blocks"},
{"role": "assistant", "content": "Picking up the green block."}
]
}
}Each snapshot replaces the previous one; retries do not append duplicate
messages. Assigned sessions can appear as public_run in the robot queue
response. Limits are 64 messages, 8,000 characters per message, 64,000 total
characters, and an 80-character model label. Roles are user, assistant,
tool, system, or developer. Send {"public_conversation": null} to withdraw
the snapshot. Never include credentials, private history, raw provider payloads,
or image bytes.
The local playground enables sharing by default. Uncheck Share conversation on
the public playground before Run, or launch with --no-share-conversation, to
keep those messages local. This does not disable robot recordings or remove task
information already published through the playground/archive. Raw local model
logs and the opt-in public plain-text snapshot are separate artifacts.