Troubleshooting

Each entry starts with what you see. The error names are the Python SDK's; the TypeScript SDK uses the same names, and the HTTP status is in brackets for direct API callers.

When you contact support, include the request id. Every API response carries an X-Request-ID header, error bodies carry request_id, and the SDKs expose it as error.request_id (Python) or error.requestId (TypeScript).

Retry or not

The status alone does not say whether to retry. A 409 can be a passing state or a final decision, and a 503 on a change may mean the change happened anyway. The SDKs have is_transient(error) (isTransient in TypeScript), which answers yes only when sending the same request again is safe and may work:

import time

import mandala_computer as mc


def with_retries(call, attempts=5):
    """Retry a call only while the SDK says the failure is transient."""
    for attempt in range(attempts):
        try:
            return call()
        except mc.MandalaError as e:
            if attempt == attempts - 1 or not mc.is_transient(e):
                raise
            time.sleep(getattr(e, "retry_after", None) or 2**attempt)


computer = mc.Client().computers.get("vm-0a1b2c3d4e5f")
png = with_retries(lambda: computer.screenshot(fresh=True))

Reads are always safe to retry. Before repeating a command, a delete or a secret change, read the current state first. Never wrap a create or clone in a retry like the one above: each call sends a new key, so sending one again can make a second billable computer. See below.

Resending a lifecycle call

Lifecycle calls (create, clone, start, stop, suspend, restart, update, move, delete, and snapshot restore and clone) take an Idempotency-Key header: the same key on the same request is carried out at most once. The SDKs send a new key on every such call. When the outcome is unknown, the error carries it as error.idempotency_key (error.idempotencyKey in TypeScript):

A key is kept for 24 hours, and the same key on a different request is refused with a 422 (code: "idempotency_key_reused"). Secret changes and commands take no key, so read the state before repeating them. The Idempotency-Key header and GET /operations in the API reference have the details.

Signing in and permissions

AuthenticationError (401). The key is missing, malformed, unknown or revoked, and error.reason says which: missing, invalid or revoked. Keys start with com_. Make a new one in Settings → Credentials → API keys. The access tokens the hosted MCP server uses are not API keys, and the REST API refuses them.

NotFoundError (404) for a computer you can see in the dashboard. The key is probably issued for a workspace, and the computer is in another one. A workspace key sees only its own workspace's computers. Use a key for that workspace, or an account-wide key.

PermissionDeniedError (403). The key is valid but its holder's role does not allow the call (a viewer cannot create computers, for example), or the account is suspended. reason: "revoked" means the authority is gone and a new key will not bring it back. A message beginning Verify your email before means the key's holder has not verified their email address, which creating a computer or a clone, an API key or an SSH key needs: verify it in the dashboard.

PlanLimitError (402). The plan's limits refused it: how many computers, their size, the total RAM or disk, or an unpaid invoice. The message names the limit. Delete or stop computers you do not need, or change plan in Settings. An agent run's 402 can instead be about AI credits or the model provider's billing; see Agents and MCP.

RateLimitError (429). Too many requests. Wait error.retry_after seconds. Every metered response carries RateLimit-Remaining, so you can slow down before you are refused: that is anything past authentication and the re-check that the key's authority still holds. A 401, a 403 from that re-check, and an unexpected 500 carry none. Screenshots and creates cost more of the budget than simple reads, and one key may use at most half of the account's budget.

Creating and starting

create() returns a computer with start_error set. The computer was made but did not boot, often because no host had room right then. It is kept, not deleted, because it exists and holds your disk. Call computer.start() again, or delete it.

ConflictError with code: template_image_preparing (409). The template's image is still being copied to a host. Wait retry_after seconds, then repeat the create with the same arguments plus the template_transfer token from the error body. Do not treat it as transient: if preparation failed, the body says so.

A custom size is slow to launch. The sizes in client.sizes.list() are kept pre-booted and launch in seconds. Any other CPU, RAM or disk combination boots from cold. Use size="…" when speed matters.

TimeoutError from launch(). The computer did not answer within the readiness budget (180 seconds by default). The error includes the computer's id, and the computer is not deleted. Inspect it with client.computers.get(id), then start or delete it. launch(timeout=600) allows longer, for example for a template that builds.

Driving the desktop

Clicks land in the wrong place. Coordinates are pixels in the computer's own screen, whose size is computer.screen (1280×800 unless you chose another resolution at create time). Three common causes:

A click or a crop is refused before it is sent. The SDKs take whole pixels only, so a coordinate or region worked out by scaling is refused when it has a fraction, and in Python whenever it is a float, even 640.0. Round it first: round(x) in Python, Math.round(x) in TypeScript.

The model clicks twice, or says its click did nothing. It was shown a stale screen. A plain screenshot() can return a frame up to 1.5 seconds old (up to 30 seconds while the computer is busy). Pass fresh=True for every screenshot that feeds a decision.

type() is refused for text with accents, CJK or emoji. Plain ASCII is typed as key presses anywhere. Other characters are typed only in supported GTK3 applications: Chromium and the Xfce terminal. Firefox, GTK4 apps and Wayland sessions refuse it, before any key is pressed. Use computer.paste(text) instead, which works in any application that accepts Ctrl+V.

type() is refused for long text or control characters. One call takes at most 400 characters, and refuses a bare carriage return and other control characters. Tab and newline are sent as keys. Split long text, or paste it (up to 8 KB).

Paste does nothing in a terminal. Terminals paste with Ctrl+Shift+V. Use computer.paste(text, shift=True) in Python or paste(text, { shortcut: 'ctrl+shift+v' }) in TypeScript.

unknown key(s): …. The key name is not one the platform knows, and nothing was pressed. Use X11 names (Return, BackSpace, Page_Down, Left) or the common ones (enter, esc, ctrl, alt, shift, super). Send a chord as separate names: key("ctrl", "l").

Horizontal scroll is refused. Left and right scrolling needs a newer hypervisor than some hosts run. The refusal says so. Scroll vertically with Shift held, which most applications treat as horizontal: scroll(x, y, direction="down", modifiers=("shift",)).

Running commands

A program with a window never appears. exec() runs as root with no display, so a GUI program has nowhere to open and exits. Use exec(cmd, desktop=True) or start_exec(cmd, desktop=True), which run as the desktop user in the graphical session, or open(url) for the browser.

GatewayTimeoutError (524) from a long exec(). On the hosted service, the proxy gives up on a request after about two minutes with no answer, even if timeout is higher. The command keeps running in the guest. Start slow commands with start_exec() and poll it, rather than waiting on exec().

ConflictError right after a timed-out command. The guest agent is still busy with the command that timed out. That is the first failure, still happening. Wait for it, or stop it, before sending more.

The output looks cut short. Output capture stops at 16 MiB. Check result.truncated. For big output, write it to a file in the guest and download the file.

pip install fails with externally-managed-environment. This is on your own machine, not the computer. Install into a virtual environment: python3 -m venv .venv && . .venv/bin/activate && pip install mandala-computer.

Suspended computers

The computer suspended itself. A computer nobody has used for the idle window is suspended to save cost. The window is 30 minutes by default, but the operator can set a host's window differently, or turn it off. Input, commands, file transfers and clipboard writes count as use and resume it. Screenshots and clipboard reads do not, so a loop that only watches the screen lets it suspend. To be sure of a computer's window, set its idle_suspend_min with computer.set_idle_suspend(minutes).

A screenshot is small and does not change. A suspended computer returns its last frame as a JPEG, marked with the X-GC-Frame: suspended header. A fresh screenshot of a suspended computer is refused with reason: "unavailable". Start the computer, or send any input, to bring it back.

ConflictError with reason: "unavailable" (409). The computer is stopped or suspended and this call does not wake it. Start it first. With reason: "starting", the computer is still booting; wait a few seconds.

Platform errors

502. The computer is up, but the agent inside it did not answer. Within a couple of minutes of a start this can be the boot still finishing. If it persists, restart the computer.

UnavailableError (503). Something the call depends on could not answer. A read can be retried shortly. A change answered with 503 may or may not have happened, so read the current state before sending it again.

Start, resume or restart answers UnavailableError (503) saying the host is low on disk space. Nothing was started, and a suspended computer keeps its saved session. Sending it again soon is refused the same way, so wait until space on that host has been freed. A create that wanted a running computer can be refused the same way, or return the new computer stopped with this message in start_error. If the message says the disk space cannot be checked right now, retry shortly. If it adds that the paused QEMU could not be stopped, stop the computer before retrying.

A running computer stops responding. Its host may be low on disk space. The platform then pauses the guests writing fastest, and when space is critically low it pauses every running computer on that host, including ones that wrote nothing. The status still reads running, but screenshots, input and commands stall or fail until space recovers and the computer resumes by itself. A suspend or memory snapshot that fails, most often while the host is low on space, can also leave the computer paused. If start, suspend and snapshot then answer UnavailableError (503) saying it is waiting for disk-space recovery, wait for it to resume by itself, or stop the computer to discard the paused session. Otherwise, suspend again once space is freed; a session that was already saved is kept and used.

OriginUnreachableError (521–523). The service could not be reached through its proxy. Retry after a short wait. If it persists, check your network first.

Agents and MCP

computer.agent() returns ConflictError. The computer must already be running, and only one run can drive a computer at a time. Start it first, or wait for the other run.

402 from an agent run. Read the message. Without a model_key the run spends the account's AI credits, and Insufficient AI credits with top_up_url means there are not enough for the next model call: top up on the billing page, or pass your own model_key. With a model_key, a 402 is usually the model provider refusing that key for billing. An OpenAI run can also end on a plan limit here, met resuming a computer that was suspended during the run; that message names the limit, as above.

AuthenticationError from an agent run. Check error.reason. revoked means your Mandala API key stopped working during the run, because it was deleted for example; the steps already taken stand. With no reason, the model provider refused a key. On your own model_key, it must be an API key for the run's provider, Anthropic or OpenAI, and it is separate from your Mandala key. On a run funded by Mandala AI credits, an error reading Mandala AI is unavailable is about Mandala's own credential, not yours.

The local MCP server does not start. Read the error it prints. The usual causes are no key (set MANDALA_API_KEY, or run mandala login), or a saved credentials file with unsafe permissions: ~/.mandala must be mode 0700 and credentials.json mode 0600, owned by you.

Still stuck? Contact support from the dashboard, with the request id and the computer's id.