> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orgo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed computers

> Put a live computer screen in your own app

Every computer streams over one WebSocket URL on `www.orgo.ai`. Any websockify-compatible VNC client connects to it, so you need no Orgo package to embed one. This page uses [noVNC](https://github.com/novnc/noVNC).

## The connection URL

```text theme={null}
wss://www.orgo.ai/desktops/{instance_id}/ws/websockify?token={password}
```

| Value | Where it comes from |
| - | - |
| `instance_id` | `POST /computers` returns it as `instance_id`. `GET /computers/{id}` returns the same value under its legacy name `fly_instance_id`. It is not the computer UUID. |
| `password` | `GET /computers/{id}` returns it as `vnc_password`. `GET /computers/{id}/vnc-password` returns it as `password`. It survives a restart but can change, so fetch it for each session rather than storing it. |

Both endpoints need your API key, so call them from your server.

## Two credentials, and which one goes in the browser

| Credential | Where it lives | What it opens |
| - | - | - |
| **API key** (`sk_…`) | Your server only. Never in browser code. | Your whole Orgo account: every computer, billing, and minting more keys. It also bypasses MFA. |
| **Computer password** | Only share with people allowed to control the computer. | One computer. |

Your server holds the API key, reads the password with it, and returns only the password to your page.

<Warning>
  **The computer password is root on that computer.** The same value opens `/ws/terminal` and `POST /api/desktops/{instance_id}/proxy/bash`, not only the screen. Anyone who can read it from your page can run commands on the computer.

  So fetch it per session from your own backend, and keep it out of your client bundle. In Next.js that means never a `NEXT_PUBLIC_*` variable: those are compiled into the JavaScript every visitor downloads. Point embeds at a disposable computer rather than one holding real work.
</Warning>

<Warning>
  An `sk_` key is **rejected** when it arrives from a browser. Passing one as `?token=` closes the socket with `4001`. Send the computer password instead.
</Warning>

## View-only sharing

A client setting such as `rfb.viewOnly = true` only disables that client's controls. It does not reduce the permission of a computer password sent to the browser; a visitor can turn the setting off.

For authenticated Orgo workspace viewers, the web proxy enforces view-only access on the server and filters input. Use workspace viewer access for this case. A public anonymous embed with a separate native VNC view-only password is not currently provided by this API.

## Embed from any domain

There is no origin allowlist on this path and nothing to register with us. The per-computer password is the credential, and it is the only thing that decides whether a connection is allowed.

## Embed with noVNC

Two files. First a server route that trades your API key for the two values the browser needs.

```ts app/api/computer/route.ts theme={null}
// SERVER. The API key never leaves this file.
export async function GET() {
  const res = await fetch(
    `https://www.orgo.ai/api/computers/${process.env.ORGO_COMPUTER_ID}`,
    {
      headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` },
      cache: 'no-store',
    },
  );
  const computer = await res.json();

  // Only the per-computer password crosses to the browser.
  return Response.json(
    { instanceId: computer.fly_instance_id, password: computer.vnc_password },
    { headers: { 'Cache-Control': 'no-store' } },
  );
}
```

<Warning>
  Gate that route with your own auth. Anyone who can call it can drive the computer.
</Warning>

Then the browser. Install noVNC with `npm install @novnc/novnc`.

```js Browser theme={null}
import RFB from '@novnc/novnc';

const { instanceId, password } = await fetch('/api/computer').then((r) => r.json());

const rfb = new RFB(
  document.getElementById('screen'),
  `wss://www.orgo.ai/desktops/${instanceId}/ws/websockify?token=${encodeURIComponent(password)}`,
  { credentials: { password } },
);

rfb.viewOnly = false;      // true renders the screen without sending input
rfb.scaleViewport = true;  // fit the screen to its container
rfb.qualityLevel = 6;      // 0 to 9
rfb.compressLevel = 2;     // 0 to 9

// Answer the VNC auth challenge when the proxy forwards it.
rfb.addEventListener('credentialsrequired', () => rfb.sendCredentials({ password }));
rfb.addEventListener('securityfailure', (e) => console.error('VNC auth failed', e.detail));

// The close code is the only signal a rejected connection gives you.
rfb._sock._websocket.addEventListener('close', (e) => console.log('closed', e.code));
```

That last line reads noVNC internals, because noVNC’s own `disconnect` event does not carry the close code.

## A failed embed is a black screen

The proxy rejects a connection by accepting the WebSocket upgrade and then closing it. noVNC therefore sees an ordinary disconnect, raises nothing, and the canvas stays black. The close code is the only way to tell a bad password from a stopped computer.

| Code | What happened | What to do |
| - | - | - |
| `4000` | The path was not recognised. | Connect to `/desktops/{instance_id}/ws/websockify`. |
| `4001` | Authentication was rejected. | Send the per-computer password, not an `sk_` key. It may have changed, so fetch it again. |
| `4003` | The computer is not running. | Start it, then reconnect once its status is `running`. |
| `4004` | No such computer. | Check `instance_id`. |
| `4006` | The credential is valid but scoped to another workspace. | Use a key for this computer’s workspace, or an account-wide key. |
| `4007` | MFA is required for this session. | Complete the MFA challenge, then reconnect. |
| `4008` | Idle for 30 minutes. | Expected. Reconnect on user activity. |
| `4010` | WebRTC is disabled for this computer. | Use the websockify path. |
| `4011` | This grant allows viewing the screen only. | Connect to `/ws/websockify`. Terminal and audio are not available. |
| `4500` | The proxy hit an internal error. | Retry. |
| `4502` | The proxy has no address for the computer. | Usually transient while a computer is starting or moving hosts. Retry. |
| `4503` | The proxy could not look up the computer. | Transient. Retry. |

Anything outside 4000-4599 is a standard WebSocket code. The proxy closes with `1000` when its own connection to the computer fails or ends, and with `1001` when it shuts down. `4003`, `4007`, `4008`, `4500`, `4502` and `4503` are worth retrying. The rest need an input changed first.

<Note>
  **Every long-lived embed eventually hits `4008`.** After 30 minutes with no human input the proxy closes the connection. The timer measures keyboard, pointer and clipboard input from your page, not pixels, so a computer an agent is driving with nobody watching still counts as idle. Treat `4008` as a clean close and reconnect on the next user action.
</Note>

## The orgo-vnc package

The published `orgo-vnc` React package (0.2.x) takes a `hostname` prop and connects to `wss://{hostname}/websockify`, from when every computer had its own host. That address no longer exists, and the package does not send the `?token=` the proxy expects. Connect with noVNC as shown in [Embed with noVNC](#embed-with-novnc) instead.

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="play" href="/quickstart">
    Full SDK setup
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Control computers programmatically
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.