Canvas sandboxes API
A private Canvas LMS on its own virtual machine, on demand, to build and test LTI 1.3 tools and Canvas API integrations. This REST API creates, starts, stops and connects to sandboxes from a script, a CI job or a coding agent. The same operations are also available as MCP tools (see the MCP section).
Quick start
- Sign in to the developer dashboard with Google, add credit, and make an API key.
- Create a sandbox with
POST /v1/sandboxes. The call answers at once; Canvas takes a couple of minutes to come up. - Poll
GET /v1/sandboxes/:iduntilstateisready. - Read the URL, the admin login, the Canvas API token and the LTI values from
GET /v1/sandboxes/:id/connection. - Stop the sandbox when you are done. Stopping keeps the disk and all Canvas data.
Base URL and authentication
Base URL: https://sandbox-api.lmsystems.ai
Send your API key as a bearer token:
Authorization: Bearer lms_sk_…
- Make and revoke API keys, and buy credit, on the dashboard. A key cannot make keys or buy credit: those routes answer 403 to a key.
- Keep the key on a server or in a script. Browsers may call the API only from the dashboard's own origin.
- Bodies and responses are JSON. Send
Content-Type: application/jsonwith a body. A body with a field the route does not know is refused with 400. - Limits: 300 requests a minute per key, and 20 sandbox creations a minute. Past a limit the API answers 429.
Errors
An error is JSON with a message, and sometimes more fields:
{ "error": "Sandbox not found" }
| Status | Meaning |
|---|---|
| 400 | The body, a query value or the Idempotency-Key header is not valid. The message names the field. |
| 401 | No API key, or a key that is wrong or revoked. |
| 402 | Not enough credit to start. The body also has code: "insufficient-credit", balanceUsd and neededUsd. |
| 403 | The account is frozen (after a payment dispute), or the route needs a dashboard sign-in. |
| 404 | No such sandbox. Another account's sandbox looks the same. |
| 409 | The request conflicts with the sandbox or the account: the account is at its sandbox limit, the sandbox is being deleted, or Canvas has not finished its first boot. |
| 429 | Too many requests. |
| 503 | There is no sandbox capacity right now. Try again later. |
| 5xx | { "error": "Unexpected server error" } |
The sandbox object
Every sandbox route answers with a sandbox, or a list of them:
{
"sandbox": {
"id": "sbx-abc123def4",
"name": "lti-test",
"size": "standard",
"state": "provisioning",
"desired": "running",
"url": "https://sbx-abc123def4.canvas.lmsystems.app",
"idleStopMinutes": 60,
"createdAt": "2026-10-30T15:00:00.000Z",
"startRequestedAt": "2026-10-30T15:00:00.000Z",
"price": { "runningPerHourUsd": 0.27, "diskPerMonthUsd": 20 }
}
}
| Field | Meaning |
|---|---|
id | sbx- and 10 letters or digits. |
name | Your label for it. |
size | standard or large. |
state | What the VM is doing now. See the states below. |
desired | What you asked for: running, stopped or deleted. |
stopReason | Only when stopped: user, idle (no sign-in traffic for idleStopMinutes), credit (balance reached $0) or frozen. |
url | The sandbox's Canvas address. |
idleStopMinutes | Minutes without signed-in traffic before it stops itself. |
createdAt, startRequestedAt, readyAt, lastActivityAt | ISO 8601 times. readyAt is when Canvas last answered; the others are set once they happen. |
deleteAt | Only when being deleted: when its data goes. |
lastError | A message, when the last VM operation failed. |
price | What this sandbox costs: runningPerHourUsd while running, diskPerMonthUsd for its disk. |
States:
state | Meaning |
|---|---|
provisioning | A new sandbox, on its first boot. |
starting | Booting from its disk. |
ready | Canvas answers. The only state in which you can use it. |
stopping | Shutting down. |
stopped | Off. The disk and all Canvas data are kept. |
resetting | Being replaced by a fresh Canvas. |
deleting | Stopped, and its data goes at deleteAt. It can be restored until then. |
Start, stop, reset, restore and delete only record what you want and answer 202. A background process then changes the VM. Wait on state, not on elapsed time.
Endpoints
List sandboxes
GET /v1/sandboxes answers { "sandboxes": [ … ] }, newest first.
Create a sandbox
POST /v1/sandboxes creates a sandbox and starts it. It answers 202 with the sandbox in state provisioning.
| Body field | Meaning |
|---|---|
name | Optional. 1–60 characters. Defaults to the id. |
size | Optional. standard (default) or large. |
idleStopMinutes | Optional. 10–1440, default 60. |
Add an Idempotency-Key header (up to 200 letters, digits and . _ : -) to make a retry safe: a repeated key returns the sandbox the first call made instead of making another.
curl -X POST https://sandbox-api.lmsystems.ai/v1/sandboxes \
-H "Authorization: Bearer $SANDBOX_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lti-test-1" \
-d '{"name":"lti-test"}'
It answers 402 when the balance is below an hour of the sandbox running, 409 when the account is at its sandbox limit (a sandbox being deleted does not count), and 503 when there is no capacity.
Get a sandbox
GET /v1/sandboxes/:id answers { "sandbox": { … } }.
Update a sandbox
PATCH /v1/sandboxes/:id with name and/or idleStopMinutes (at least one) answers { "sandbox": { … } }.
Start a sandbox
POST /v1/sandboxes/:id/start runs a stopped sandbox. Its Canvas and data are as you left them. It needs credit for at least an hour of running (402 if not) and answers 409 if the sandbox is being deleted.
Stop a sandbox
POST /v1/sandboxes/:id/stop stops it. Running time stops being billed. The disk and all Canvas data are kept, and the disk is still billed until the sandbox is deleted.
Reset a sandbox
POST /v1/sandboxes/:id/reset replaces the sandbox's Canvas with a fresh one, with a new admin password and API token. Everything in the old Canvas is lost. It needs the same credit as a start.
Delete and restore a sandbox
DELETE /v1/sandboxes/:idstops the sandbox and deletes it with all its data 24 hours later (deleteAt).DELETE /v1/sandboxes/:id?immediately=truedeletes it at once. This cannot be undone.POST /v1/sandboxes/:id/restorecancels a delete within the 24 hours. The sandbox comes back stopped. It answers 409 if the sandbox is not being deleted, or if its disk is already going.
Connection details
GET /v1/sandboxes/:id/connection has everything you need to use the sandbox's Canvas. It is never cached.
{
"id": "sbx-abc123def4",
"state": "ready",
"url": "https://sbx-abc123def4.canvas.lmsystems.app",
"loginUrl": "https://sbx-abc123def4.canvas.lmsystems.app/__lmsystems/enter?t=…",
"loginUrlExpiresAt": "2026-10-30T15:13:00.000Z",
"canvas": {
"adminEmail": "admin@canvas.local",
"adminPassword": "…",
"apiToken": "…",
"apiBase": "https://sbx-abc123def4.canvas.lmsystems.app/api/v1"
},
"lti": {
"issuer": "https://canvas.instructure.com",
"authorizationEndpoint": "https://sbx-abc123def4.canvas.lmsystems.app/api/lti/authorize_redirect",
"tokenEndpoint": "https://sbx-abc123def4.canvas.lmsystems.app/login/oauth2/token",
"jwksUrl": "https://sbx-abc123def4.canvas.lmsystems.app/api/lti/security/jwks"
}
}
loginUrlopens Canvas in a browser, signed in as the admin. It works for 10 minutes (loginUrlExpiresAt).canvas.apiTokenis an admin token for Canvas's own REST API atcanvas.apiBase. Send it asAuthorization: Bearer <token>.ltihas the LTI 1.3 platform values to register your tool with.noteappears when the sandbox is notready: the values are from its last boot, and the sandbox must be started to use them.- It answers 409 while Canvas has not finished its first boot (the body has
state), and when the sandbox is being deleted. - The admin password and API token are secrets. Do not log, print or commit them.
Account
GET /v1/balanceanswers{ "balanceUsd": 12.5, "credits": [{ "amountUsd", "kind", "at" }] }: the balance and the last 20 credit entries.GET /v1/usage?month=YYYY-MManswers{ "month", "sandboxes": [{ "sandboxId", "runningHours", "diskGbHours", "chargedUsd" }] }. The month defaults to the current one.GET /v1/pricingneeds no key. It answers{ "sizes": [{ "id", "vcpus", "memoryGb", "runningPerHourUsd" }], "disk": { "gb", "perMonthUsd" }, "billing" }.
Example: create a sandbox and use its Canvas
export SANDBOX_KEY=lms_sk_… # from the dashboard
BASE=https://sandbox-api.lmsystems.ai
AUTH="Authorization: Bearer $SANDBOX_KEY"
# 1. Create it
id=$(curl -s -X POST "$BASE/v1/sandboxes" -H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: lti-test-1" -d '{"name":"lti-test"}' | jq -r .sandbox.id)
# 2. Wait until Canvas answers
until [ "$(curl -s "$BASE/v1/sandboxes/$id" -H "$AUTH" | jq -r .sandbox.state)" = ready ]; do sleep 10; done
# 3. Get the connection details (they hold secrets)
curl -s "$BASE/v1/sandboxes/$id/connection" -H "$AUTH" > connection.json
# 4. Call Canvas's own API with the admin token
curl -s "$(jq -r .canvas.apiBase connection.json)/courses" \
-H "Authorization: Bearer $(jq -r .canvas.apiToken connection.json)"
# 5. Stop it when you are done
curl -s -X POST "$BASE/v1/sandboxes/$id/stop" -H "$AUTH"
Timing, idle stop and billing
- A new or reset sandbox usually reaches
readyin about 2 minutes. Allow up to 7. A start fromstoppedusually takes about 2 minutes. Allow up to 5. Poll every 5–10 seconds. - A sandbox stops itself after
idleStopMinutes(10–1440, default 60) without signed-in traffic: a browser session, or a request that carries anAuthorizationheader. It stops withstopReason: "idle", and its data is kept. - It also stops when the balance reaches $0 (
credit) and when the account is frozen (frozen). - Running time is billed per second. The disk is billed from creation until the sandbox is deleted, including while it is stopped, so a balance can go below $0.
GET /v1/pricinghas the prices. - Create, start and reset need credit for at least one hour of running. Credit is prepaid and is bought on the dashboard.
MCP
The same operations are MCP tools at https://sandbox-api.lmsystems.ai/mcp (HTTP transport). Use an API key; the MCP endpoint takes no dashboard sign-in. The tools are list_sandboxes, create_sandbox, get_sandbox, wait_for_sandbox, start_sandbox, stop_sandbox, reset_sandbox, delete_sandbox, restore_sandbox, get_connection and get_account. To add the server to Claude Code:
claude mcp add --transport http canvas-sandbox https://sandbox-api.lmsystems.ai/mcp --header "Authorization: Bearer $LMSYSTEMS_API_KEY"
Notes for AI agents
- Ask the user before you reset or delete a sandbox, or send
immediately=true. They destroy data. - Never print, log or commit the admin password or the Canvas API token. Read them from
connectioninto a variable or a file that is not committed. - Wait on
state === "ready", and checklastErrorif a sandbox stays in one state for longer than the times above. - Stop a sandbox you are done with. A running sandbox is billed until it idles out.