# Connect your endpoint

Keep weights on your own infrastructure — the facility streams observations to your HTTPS URL and logs round-trip time per step.

*Provider workflow: Your endpoint.*

Endpoint mode inverts the trust arrangement of the other two
packagings: your weights never enter the facility. You host inference;
the cell streams observations to your URL and expects actions back, per
step, on a declared latency budget. What you give up is the network —
it is now inside the control loop — and much of this page is about
being honest with yourself about that.

## Prerequisites

- **An HTTPS inference URL** reachable from the facility's egress.
- **Capacity for the declared action rate.** `joint_delta_50hz` is an
  action every 20 ms, sustained for every episode of the run —
  provision for the whole job, not for a demo.
- **Your own version identity.** Put the deployment version in the URL
  path or hostname — `https://inference.acme.ai/v7/act` — because the
  run records the URL. That is how a result stays attributable to a
  deployment: if `v7` and `v8` share one URL behind a load balancer, no
  artifact can say which one earned the rate.

### The protocol, honestly

The policy object carries three fields — `url`, `latency_budget_ms`,
`fallback` — and nothing else. There are no auth headers, bearer
tokens, or mTLS fields today. Treat the URL as the credential: make it
unguessable and per-run
(`https://inference.acme.ai/v7/act/8f3a2c91d4e6`), rotate it after the
run, and enforce your own network controls — allowlists, WAF rules — on
your side. Do not assume any facility-side security guarantee beyond
"the URL is stored with the run".

The interface itself is behavioral: the cell runtime feeds your server
the observations named by the declared `observation_contract` and
expects actions in the declared `action_space`, at its declared rate.
Between episodes the runtime signals an episode boundary, and your
server clears its per-episode state — physical scene reset is the
cell's job (scripted or teleop-assisted), never your policy's. A formal
wire-level spec — routes and schemas — is not yet published.

### What leaves the cell

Exactly the feeds named by the declared `observation_contract`, plus
the proprioception schema — per step, to your URL. `droid-3cam` means
three RGB views and joint state, and nothing else. Round-trip time is
logged per step to `/events`. In return, nothing is cached: endpoint
mode creates no Roborama copy of your weights at all — the same IP
posture the [data contract](/docs/concepts/data-contract/) applies to
your episodes.

### Latency and the fallback

Your network is in the loop, so the budget is explicit:
`latency_budget_ms=80` means any step whose round trip exceeds 80 ms
triggers the declared `fallback`. `"halt"` freezes the arm and ends the
episode, which lands in its own cluster instead of polluting the
physics statistics; `"teleop"` is the other documented value.
Persistent unreachability fails the run with `policy_endpoint_timeout`.
Budget from your p99, not your median — the
[policies page](/docs/concepts/policies/) shows the arithmetic of a
40 ms median with a 200 ms p99, and it is not on your side.

## Connect

*Example: connect a hosted endpoint*

**Python**

```python
import roborama  # reads ROBORAMA_API_KEY from the environment

policy = roborama.Policy.endpoint(
    url="https://inference.acme.ai/v7/act",
    latency_budget_ms=80,
    fallback="halt",
)

run = roborama.run(
    robot="g1-edu-pro@fw2.3",
    environment="kitchen-std@v1.2",
    policy=policy,
    task="load_dishwasher@v2",
    episodes="auto(ci=0.95, moe=0.03)",
    max_budget_usd=4000,
)

result = run.result()
# per-step endpoint RTT is logged to /events for this run
print(run.id, result.success_rate, result.ci95)
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk"; // reads ROBORAMA_API_KEY

const roborama = new Roborama();

const run = await roborama.runs.create({
  robot: "g1-edu-pro@fw2.3",
  environment: "kitchen-std@v1.2",
  policy: {
    type: "endpoint",
    url: "https://inference.acme.ai/v7/act",
    latency_budget_ms: 80,
    fallback: "halt",
  },
  task: "load_dishwasher@v2",
  episodes: "auto(ci=0.95, moe=0.03)",
  max_budget_usd: 4000,
});

const result = await run.result();
// per-step endpoint RTT is logged to /events for this run
console.log(run.id, result.success_rate, result.ci95);
```

**cURL**

```bash
# weights stay on your side — the cell streams observations
# to the URL below and expects actions back, per step
curl -sS https://api.roborama.com/v1/runs \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "robot": "g1-edu-pro@fw2.3",
    "environment": "kitchen-std@v1.2",
    "policy": {
      "type": "endpoint",
      "url": "https://inference.acme.ai/v7/act",
      "latency_budget_ms": 80,
      "fallback": "halt"
    },
    "task": "load_dishwasher@v2",
    "episodes": "auto(ci=0.95, moe=0.03)",
    "max_budget_usd": 4000
  }'

# result for the run id returned above — per-step endpoint RTT
# is logged to /events for this run
curl -sS https://api.roborama.com/v1/runs/run_8844 \
  -H "Authorization: Bearer $ROBORAMA_API_KEY"
```

**Agent (tool-use payload)**

```json
[
  {
    "type": "tool_use",
    "name": "roborama_run",
    "input": {
      "robot": "g1-edu-pro@fw2.3",
      "environment": "kitchen-std@v1.2",
      "policy": {
        "type": "endpoint",
        "url": "https://inference.acme.ai/v7/act",
        "latency_budget_ms": 80,
        "fallback": "halt"
      },
      "task": "load_dishwasher@v2",
      "episodes": "auto(ci=0.95, moe=0.03)",
      "max_budget_usd": 4000
    }
  },
  {
    "type": "tool_use",
    "name": "roborama_watch_run",
    "input": { "run_id": "run_8844" }
  }
]
```

## Validate

Local first: `ROBORAMA_MOCK=1` exercises the full loop against packaged
fixtures — no API key, no charge.

*Example: dry-run in mock mode*

**Python**

```python
import roborama

# Mock mode: no API key, no hardware. The whole loop runs
# offline against packaged fixtures — or set ROBORAMA_MOCK=1.
client = roborama.Client(mock=True)

quote = client.quote(
    robot="aloha2-pro",
    environment="cell-a",
    episodes=300,
)
print(quote)
# {robot_hours: 10.0, env_hours: 10.0, usd: 860, queue_eta: "2h"}

run = client.run(
    robot="aloha2-pro@fw1.1",
    environment="cell-a@v1.0",
    policy=roborama.Policy.checkpoint(
        hf="acme/act-so101-pick-v2",
        runtime="lerobot",
    ),
    task="pick_place@v1",
    episodes=300,
    max_budget_usd=1000,
)
print(run.result())
# n=300  rate=0.843  ci95=(0.797, 0.881)
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk";

// Mock mode: no API key, no hardware. The whole loop runs
// offline against packaged fixtures — or set ROBORAMA_MOCK=1.
const roborama = new Roborama({ mock: true });

const quote = await roborama.quote({
  robot: "aloha2-pro",
  environment: "cell-a",
  episodes: 300,
});
console.log(quote);
// { robot_hours: 10.0, env_hours: 10.0, usd: 860, queue_eta: "2h" }

const run = await roborama.runs.create({
  robot: "aloha2-pro@fw1.1",
  environment: "cell-a@v1.0",
  policy: {
    type: "checkpoint",
    hf: "acme/act-so101-pick-v2",
    runtime: "lerobot",
  },
  task: "pick_place@v1",
  episodes: 300,
  max_budget_usd: 1000,
});
console.log(await run.result());
// n=300  rate=0.843  ci95=(0.797, 0.881)
```

**cURL**

```bash
# Mock mode is an SDK feature — curl always talks to the live
# API. ROBORAMA_MOCK=1 flips any SDK entry point to packaged
# fixtures: no API key, no hardware, no spend.
ROBORAMA_MOCK=1 python - <<'PY'
import roborama

quote = roborama.quote(
    robot="aloha2-pro", environment="cell-a", episodes=300)
print(quote)

run = roborama.run(
    robot="aloha2-pro@fw1.1",
    environment="cell-a@v1.0",
    policy=roborama.Policy.checkpoint(
        hf="acme/act-so101-pick-v2", runtime="lerobot"),
    task="pick_place@v1",
    episodes=300,
    max_budget_usd=1000,
)
print(run.result())
PY
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_quote",
  "input": {
    "robot": "aloha2-pro",
    "environment": "cell-a",
    "episodes": 300
  }
}
```

At submission, the declared contracts are validated server-side; a
mismatch is `contract_validation_failed` (422), and a rejected
submission costs nothing. Then, at cell startup, the runtime performs a
reachability exercise against your URL *before episode 1*: sustained
failure lands as `policy_endpoint_timeout`, and no episodes bill. A
dead DNS record costs you a queue slot, not money.

## Quote

Endpoint runs meter like any other — robot-hours and environment-hours;
your inference compute stays on your own bill. Quote first, and read
the queue ETA as a capacity question: can the deployment hold the
declared rate for that long?
([billing & budgets](/docs/guides/billing-and-budgets/))

*Example: quote before you run*

**Python**

```python
import roborama  # reads ROBORAMA_API_KEY from the environment

quote = roborama.quote(
    robot="g1-edu-pro",
    environment="kitchen-std",
    episodes=600,
)
print(quote)
# {robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h"}
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk"; // reads ROBORAMA_API_KEY

const roborama = new Roborama();

const quote = await roborama.quote({
  robot: "g1-edu-pro",
  environment: "kitchen-std",
  episodes: 600,
});
console.log(quote);
// { robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h" }
```

**cURL**

```bash
curl https://api.roborama.com/v1/quote \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }'
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_quote",
  "input": {
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }
}
```

## Evaluate

Submission is an ordinary [`run()`](/docs/primitives/run/). Watching
live matters more here than in the other packagings — per-step RTT
lands in `/events` as it happens, so you can catch a saturating
deployment while the run is still cheap to stop:

*Example: watch it live*

**Python**

```python
import roborama  # reads ROBORAMA_API_KEY from the environment

run = roborama.runs.get("run_8842")

for event in run.watch():   # live: episode ticker + WebRTC stream URLs
    print(event.episode, event.status)

stream = roborama.streams.get("cell-g1-04")
print(stream)
# {webrtc: "wss://streams.roborama.com/cells/cell-g1-04/webrtc",
#  mjpeg: "https://streams.roborama.com/cells/cell-g1-04/mjpeg",
#  viewer_token: "vt_7Kq2mHentXw4"}
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk"; // reads ROBORAMA_API_KEY

const roborama = new Roborama();

const run = await roborama.runs.get("run_8842");

for await (const event of run.watch()) {
  // live: episode ticker + WebRTC stream URLs
  console.log(event.episode, event.status);
}

const stream = await roborama.streams.get("cell-g1-04");
console.log(stream);
// { webrtc: "wss://streams.roborama.com/cells/cell-g1-04/webrtc",
//   mjpeg: "https://streams.roborama.com/cells/cell-g1-04/mjpeg",
//   viewer_token: "vt_7Kq2mHentXw4" }
```

**cURL**

```bash
# live event stream (server-sent events): episode ticker + status
curl -N https://api.roborama.com/v1/runs/run_8842/events \
  -H "Authorization: Bearer $ROBORAMA_API_KEY"

# cell video stream descriptor
curl https://api.roborama.com/v1/streams/cell-g1-04 \
  -H "Authorization: Bearer $ROBORAMA_API_KEY"
```

**Agent (tool-use payload)**

```json
[
  {
    "type": "tool_use",
    "name": "roborama_watch_run",
    "input": { "run_id": "run_8842" }
  },
  {
    "type": "tool_use",
    "name": "roborama_get_stream",
    "input": { "cell": "cell-g1-04" }
  }
]
```

## Inspect results

Alongside n, the rate, and the Wilson interval, the episode record is
where endpoint mode's questions get answered: halted episodes carry
their own cluster, and the `/events` channel holds the per-step RTTs,
so a latency problem and a policy problem are distinguishable after the
fact. `run.report(format="pdf")` cites the same record — channels and
retention are in the [data contract](/docs/concepts/data-contract/).

## Compare versions

`v7` against `v8` is two URLs under the same declared configuration —
which is why the version belongs in the URL. Submit both to
[`compare()`](/docs/primitives/compare/) for paired episodes: same
scenes for both deployments, roughly half the episodes, and a verdict
with an interval on the difference.

## Troubleshoot

| Code | Symptom | Fix |
| --- | --- | --- |
| `policy_endpoint_timeout` | run halted — steps persistently over `latency_budget_ms`, or the startup reachability exercise failed | budget from your p99, not your median; provision capacity for the declared rate sustained, not burst; fix the endpoint, then resubmit |
| `contract_validation_failed` | 422 at submission; nothing ran | the declared `action_space` or `observation_contract` doesn't match the pinned robot and scene |
| `estop_triggered` | run stopped by a hardware or safety emergency stop | the episode is marked `estop`, the cell is inspected, partial results are kept — contact support if unexpected |
| `budget_exceeded` | run halted at `max_budget_usd`; partial results kept | raise the cap and resubmit if the interval is too wide |

## Where next

- [Policies](/docs/concepts/policies/) — the three packagings, and the latency callout this page keeps pointing at.
- [Data contract](/docs/concepts/data-contract/) — the IP posture endpoint mode is built on.
- [Run your container](/docs/guides/run-your-container/) — the opposite trade: ship the image, take the network out of the loop.
- [Error codes](/docs/errors/) — `policy_endpoint_timeout` in the full catalogue.
