---
title: "The shape of a sandbox API call · intentic"
description: "Input rides the query string on a GET and a JSON body on everything else, including DELETE. The base address, the two conventions, and the routes that answer bytes instead of JSON."
url: "https://intentic.dev/api/calls/"
updated: "2026-08-24"
---

Start here

# The shape of a call

There is no envelope and no versioning scheme. The contract's path is the URL, the body you send is the declared shape, and two conventions cover every route on the site.

**On this page (5 sections)**

- [Where the sandbox is](#where-it-is)
- [Two conventions, all 261 routes](#two-conventions)
- [Path parameters](#path-parameters)
- [Routes that answer bytes](#not-json)
- [Old daemon, new client](#old-daemon)

## Where the sandbox is

Each sandbox answers on its own hostname, `https://sandbox-<id>.<zone>`, over its private tunnel. The app shows the exact address under **Sandbox → Overview**. One route stays open, and it is the one every launch script and readiness loop asks first.

Is a daemon there

```bash
curl https://sandbox-a1b2c3d4e5f6.intentic.dev/health

{ "ok": true, "sandboxId": "a1b2c3d4e5f6", "boot": { … }, "announce": { … } }
```

The sandbox id in that answer is how a caller proves it reached *this* daemon rather than something else on the same port. On the machine actually running the sandbox there is also `https://local-<id>.<zone>`, a public name that resolves to the loopback address: a real certificate on `127.0.0.1`, so a browser on that machine skips the round trip out through the tunnel and back.

## Two conventions, all 261 routes

| Convention | Detail |
| --- | --- |
| Input on GET | The query string. |
| Input on everything else | A JSON body. Including `DELETE`, which sends its parameters in the body rather than the query. |
| Output | JSON, except the byte routes below and the [streams](https://intentic.dev/api/streams/). |
| Failure | A JSON object with a `message`, never an empty body. See [when a call fails](https://intentic.dev/api/errors/). |

Two calls

```bash
SANDBOX=https://sandbox-a1b2c3d4e5f6.intentic.dev

# GET: input rides the query string
curl "$SANDBOX/workspace/file?path=README.md" \
 -H "x-intentic-control: $INTENTIC_TOKEN"

# Everything else: input rides the JSON body, including DELETE
curl -X POST "$SANDBOX/git/root/commit" \
 -H "x-intentic-control: $INTENTIC_TOKEN" \
 -H "content-type: application/json" \
 -d '{"message":"Fix the flaky parser test"}'
```

## Path parameters

A templated segment is written `{repo}` or `{id}` throughout this reference, and is URL-encoded in the request. For every git route, `root` is the workspace repository itself and any other value is a repository's directory relative to the workspace root.

## Routes that answer bytes

Four routes fall outside the JSON contract entirely, because bytes are not a shape a JSON contract can describe.

| Route | Why it's different |
| --- | --- |
| `GET /workspace/raw` | A file's bytes with a content type worked out from its extension: how an image or a PDF is previewed, where the text route would corrupt it. |
| `GET /workspace/media` | A streamed byte range for audio and video. A media element cannot send a header, so this takes a short-lived ticket minted by `POST /workspace/media-ticket`, which *is* in the reference. |
| `POST /workspace/upload` | Raw bytes, in parts, so a large file stays under the edge's body limit. |
| `GET /extensions/{id}/bundle` | An installed extension's code, as bytes. |

## Old daemon, new client

Your browser is routinely newer than the daemon it is talking to: one released app plane serves every sandbox, whatever image each user last pulled. That is supported rather than tolerated, and it is why the identity route and the event stream's opening frame both advertise the route names this particular daemon implements. A client compares that list against the contract it was built with, so a route the daemon predates surfaces as a named gap instead of a 404 nobody can attribute.

Pin nothing; ask.

More in Start here

[Previous ← Authorising a call](https://intentic.dev/api/auth/)[Next When a call fails →](https://intentic.dev/api/errors/)
