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):
- Any lifecycle call answered with a 5xx, a create or clone included. Send the same call again with the same key (
idempotency_key=e.idempotency_keyin Python,{ idempotencyKey: e.idempotencyKey }in TypeScript), waiting a few seconds between tries. If it still answers a 5xx after three tries, stop and ask the user rather than keep sending. Iflaunch's own create answered the 5xx (the error does not readlaunch of vm-… failed), resend it throughlaunchwith the key from SDK 0.8.0. On an earlier SDK, do not calllaunchagain: its create's outcome is unknown, so look it up by the error's key as below, and if it made the computer, read it withclient.computers.get(id), then start it, wait on it, or delete it. If the platform released the key, because the call was refused before it was sent anywhere or was a create or clone the host confirms it refused before building anything, it is carried out now, once. If it did not, the resend answers a 409 withcode: "idempotency_outcome_unknown", and only then read the computer or look the operation up, as below. A 5xx made in front of the platform (a524from the edge, say) can come while the first call is still running: then the resend answers a 409 withcode: "idempotency_in_progress", which you handle as below. Once that call has ended, the resend gets what its ending left: the first call's own answer if it succeeded, without its desktop credentials (read the computer withclient.computers.get(id)for them; an answer too large to keep comes back as{operation_id, operation}instead), a fresh attempt if it was refused, or a 409 withcode: "idempotency_outcome_unknown"if it ended without an answer. If the error carries no key, do not send it again. - A create or clone whose outcome is unknown: a dropped connection, or a 409 with
code: "idempotency_outcome_unknown"or"idempotency_in_progress". Do not create again on your own. Look it up withGET /operations?idempotency_key=<key>(client.operations.list(idempotency_key=...)), or withGET /operations/{id}for theoperation_idthe error names. If itsstateissucceeded, use the computer itscomputer_idnames, and read it withclient.computers.get(id)for its desktop credentials. If it ispendingorrunningafter a 409idempotency_outcome_unknown, it will not settle for about an hour, and it means the outcome is unknown: stop and ask the user now. If it ispendingorrunningafter a dropped connection or a 409idempotency_in_progress, the call may still be running: wait a few seconds and look again, and if it has not settled after five minutes, stop and ask the user. Anything else (failedfor any reason, no operation found, no key to look it up by, or a lookup that fails): stop and ask the user before creating another computer. - A failed
launch. An error that readslaunch of vm-… failedmeans the computer already exists: its create succeeded and a later step (the start, a wait) failed. Never create it again, and never send that error's key on a create, which is refused with a 422 or makes a second computer. Read it withclient.computers.get(id), then start it, wait on it, or delete it.computers.launchaccepts the key from SDK 0.8.0 (idempotency_key=in Python,{ idempotencyKey }in TypeScript) and sends it on its create only. Before 0.8.0 neither SDK'slaunchaccepts one: Python's refuses the argument, and TypeScript's drops it silently and sends a new key, which can make a second computer, so on an earlier SDK a create that answered a 5xx is looked up as above. - Any other lifecycle call, after a dropped connection or a 409 with
code: "idempotency_in_progress". Send the same call again with the same key, for examplecomputer.start(idempotency_key=e.idempotency_key). It is not carried out a second time; you get the first call's answer, oridempotency_in_progressagain while it is still running. If the error carries no key (idempotency_keyisNone,idempotencyKeyisundefined), do not send the call again: the SDK would send a new key and carry it out twice. The call that failed may be the SDK's read of the computer after the action succeeded, so read the computer to see whether it took effect. - Any other lifecycle call, after a 409 with
code: "idempotency_outcome_unknown", including one a resend after a 5xx answered. The platform did not hear how the call ended, so do not send it again. Read the computer, or look the call up withGET /operations?idempotency_key=<key>, to see whether it took effect. An operation that is stillpending, or isfailedwitherror.code: "lost", means the platform does not know how the call ended, not that it failed: it may have taken effect.
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:
- The model was told a different screen size. Pass the real one.
- The screenshots were downscaled, with
screenshot(width=…)or by your own code, before the model saw them. Scale the model's coordinates back up by the same factor and round them to whole pixels. - The model uses a normalized grid. Gemini answers on 0–999; multiply by the width or height, divide by 1000 and round down to a whole pixel.
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.