---
title: "Authorising a sandbox API call · intentic"
description: "Two credentials reach an intentic sandbox: a session for a signed-in person and a control token for a program. What each one is, how to get it, and what the four scopes reach."
url: "https://intentic.dev/api/auth/"
updated: "2026-08-24"
---

Start here

# Authorising a call

Two credentials reach a sandbox, and the difference between them is whether a person is present. Everything else about authorisation follows from that.

**On this page (5 sections)**

- [Two kinds of caller](#two-callers)
- [A person, in a browser](#a-person)
- [A program, anywhere](#a-program)
- [What a scope reaches](#scopes)
- [Open on purpose](#open-on-purpose)

## Two kinds of caller

A browser has a signed-in human behind it and can be sent through a sign-in flow. A script cannot. So there are two credentials, one for each, and every route on this site accepts either.

The same call, both ways

```bash
# A program, anywhere: the control token
curl "$SANDBOX/git/root/status" -H "x-intentic-control: $INTENTIC_TOKEN"

# A person, in a browser: the session minted from their sign-in
curl "$SANDBOX/git/root/status" -H "authorization: Bearer $SESSION"
```

## A person, in a browser

The sandbox authenticates the end user **directly against Google**: the browser presents a Google identity token and the daemon verifies its signature against Google's published keys. The platform never holds or signs it, which is why a compromise of the platform cannot command your sandbox. The first verified identity to arrive becomes the owner, and the owner grants everyone else a role that decides how far they reach.

`POST /system/session` exchanges that token for a session the daemon minted, and that session is what every steady-state call carries as `authorization: Bearer …`. Calling the same route again with a session that has not expired renews it, so a long-lived tab never has to sign in twice.

## A program, anywhere

Anything outside a browser presents a **control token** in the `x-intentic-control` header instead. A control token is shown once, when it is minted. The daemon keeps only its hash, so it cannot be recovered and it can be revoked at any moment.

Minting one as the owner

```bash
curl -X POST "$SANDBOX/system/control/tokens" \
 -H "authorization: Bearer $OWNER_SESSION" \
 -H "content-type: application/json" \
 -d '{"label":"nightly CI","scope":"read"}'

{ "id": "6f1c…", "token": "ict_9wQ…" }
```

The app mints one for you under **Sandbox → Computers → Editor bridge**, at `editor` scope, together with the settings an editor needs to use it. Any other scope is minted through the route above, which requires an owner session.

## What a scope reaches

A scope is chosen when the token is minted and stored with it, rather than worked out from the caller. That is deliberate: the daemon cannot tell an editor from a build job from a cron script, because all three are just a program holding a secret. The only honest moment to decide how far one reaches is when a person decides it.

| Scope | Reaches |
| --- | --- |
| `editor` | One conversation: run a turn, answer a card it parked on, read transcripts, search the tree. What an editor bridge holds. It cannot see the fleet and it cannot land work. |
| `read` | Observation only: the fleet, past sessions, workspace search, listening ports. The one genuinely narrow rung, which is why it exists separately rather than as a politeness. |
| `drive` | Everything read sees, plus making an agent work: start, answer, steer and stop a turn. Stops short of anything that moves code into the main tree. A stolen token at this rung is the agent's reach. |
| `land` | Everything drive does, plus merging a conversation's worktree into the main tree, and discarding one. Separate because the usual arrangement is a program that works and a person who decides. |

Worth saying plainly: at `drive` and above, a stolen token is the agent's reach, because driving an agent means editing files and running commands in this sandbox. `read` exists separately for exactly that reason rather than as a politeness. A token presented on a route outside its scope gets a [403 that says so](https://intentic.dev/api/errors/), never a confusing 401.

## Open on purpose

A handful of routes check no credential, because the caller provably cannot present one: `/health`, the web-chat widget's routes, an automation's webhook, a forge's pipeline webhook, a workflow gate, and the enrolment routes a new computer redeems a one-time pairing token on. Each carries its own narrower check instead of the general one.

Only `/health` is missing from this reference, and that is on purpose too: it is not part of the contract. It exists so a script can tell a live sandbox from a dead port, and it deliberately checks nothing.

More in Start here

[Previous ← Overview](https://intentic.dev/api/)[Next The shape of a call →](https://intentic.dev/api/calls/)
