# Machine as a Backend

Start and manage Machine sessions from your backend with explicit connector, model, context, and secret scope.

Canonical page: https://themachine.myapps.ai/docs/backend

Use a Machine API key to start sessions from your server. Each session has one
Machine owner, one project, and one cost record.

Your application owns its customer identifiers and metadata. Store the
relationship between your customer and the returned `session_id` in your
application database.

## 1. Get an API key

Create a personal access token or a service-account credential
(`kortix_sa_…`). Both authenticate a programmatic session-create request. The
API derives `origin: "backend"` from the credential type.

Create a personal access token in the web app under **Settings → API keys**.
New keys start with `mach_pat_`. Keys that start with `kortix_pat_`, such as
the one `machine login` saves and keys made before `mach_pat_` existed, work
the same way.

```bash
export MACHINE_API_URL="https://machineapi.myapps.ai/v1"
export MACHINE_API_KEY="mach_pat_…"
export MACHINE_PROJECT_ID="…"
```

For a self-hosted stack, point `MACHINE_API_URL` at your own API's `/v1` base.
These are your own shell variables; any names work.

Use a service account when you need an independently managed principal. Grant
that service account the required project actions before use.

## 2. Start a session

### Create with HTTP

```bash
curl -X POST "$MACHINE_API_URL/projects/$MACHINE_PROJECT_ID/sessions" \
-H "Authorization: Bearer $MACHINE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"agent_name": "support",
"opencode_model": "kortix/glm-5.2",
"runtime_context": { "ticket_id": "ticket-123" },
"connector_bindings": {
"gmail": { "connection_id": "<connection-id>" }
},
"secrets": ["STRIPE_KEY"]
}'
```

### Create with the SDK

```ts
import { createScopedKortix } from '@kortix/sdk/server';

const kortix = createScopedKortix({
backendUrl: process.env.MACHINE_API_URL!,
getToken: async () => process.env.MACHINE_API_KEY!,
});

const session = await kortix.project(projectId).sessions.create({
agent_name: 'support',
opencode_model: 'kortix/glm-5.2',
runtime_context: { ticket_id: 'ticket-123' },
connector_bindings: {
gmail: { connection_id: connectionId },
},
secrets: ['STRIPE_KEY'],
});
```

Use `createScopedKortix` when one server process handles concurrent requests.
Each client keeps its token and runtime state request-scoped.

Store the `session_id` returned by either create call:

```bash
export SESSION_ID="<session-id>"
```

### Session-create fields

| Field                | Contract                                                                                                                                                |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_name`         | Selects a declared OpenCode agent.                                                                                                                      |
| `opencode_model`     | Selects the initial OpenCode model. An unavailable model returns `400 INVALID_SESSION_MODEL`.                                                          |
| `runtime_context`    | Stores non-secret scalar context. The API rejects credential-like keys, more than 64 entries, or more than 16 KiB.                                      |
| `connector_bindings` | Maps a connector slug to one strategy-compatible `connection_id`. The credential stays outside the sandbox.                                  |
| `inherit_unbound`    | Keeps strategy-based default resolution for connectors omitted from an explicit binding map. The default is `false`.                            |
| `secrets`            | Narrows the selected agent's project-secret grant. An empty list delivers no project secrets. Only backend-origin callers can set it.                   |
| `require_connectors` | Adds mandatory connectors for this create request. Missing connections return `409 CONNECTOR_CONNECTION_REQUIRED`, and unconfigured slugs return `409 REQUIRED_CONNECTOR_CONNECTION_UNAVAILABLE`, before the session row is inserted and before sandbox startup. |

## 3. Configure connectors and connections

A connector defines the tool surface. It contains a project-unique
slug, display name, provider app, authorization strategy, and policies.

A connection stores one connected account or credential for that connector.
Every connection inherits the connector's policies.

The authorization strategy has two values:

- `project` accepts active project connections.
- `user` accepts only an active connection owned by the acting project
  member.

A service account has no member identity, so it cannot use a member's `user`
connection. Use `project` connectors for service-account sessions.
A personal access token can use an eligible `user` connection owned by the
token's member.

```yaml
connectors:
  - slug: gmail-read
    name: Gmail read only
    provider: pipedream
    app: gmail
    authorization_strategy: project
    policies:
      - match: search_email
        action: always_run

agents:
  support:
    connectors: [gmail-read]
    connectors_required: [gmail-read]
```

Every agent in the project can call every connector connected on it. An
agent's `connectors` list is kept in the manifest but no longer narrows that.
`connectors_required` still applies: a session for that agent must resolve an
active connection before it starts.

The SDK exposes connections under `project.connectors.connections`:

```ts
const connection = await kortix.project(projectId).connectors.connections.reconcile({
  connector_alias: 'gmail-read',
  owner_type: 'project',
  label: 'Support inbox',
});

await kortix
  .project(projectId)
  .connectors.connections.updateCredential(connection.connection_id, {
    value: credential,
    kind: 'secret',
  });

await kortix.project(projectId).connectors.connections.activate(connection.connection_id);
```

For a Pipedream OAuth connection, call `pipedreamConnect()` and
`pipedreamFinalize()`. Do not place its provider token in
`updateCredential()`.

> **Info**
> The connection object and new session binding input use `connection_id`.
> `authorization_id` remains a deprecated SDK input alias.

## 4. Read and replace session scope

The session scope is authoritative server state. `secrets_allowlist` contains
the session's stored narrowing. A `null` value means the agent grant applies.
`connector_bindings` contains the materialized connection
selection.

```bash
curl -sS \
  "$MACHINE_API_URL/projects/$MACHINE_PROJECT_ID/sessions/$SESSION_ID/scope" \
  -H "Authorization: Bearer $MACHINE_API_KEY"
```

```ts
const scope = await kortix.session(projectId, sessionId).scope();
```

Replace scope with `PUT` or `rescope()`:

```ts
const nextScope = await kortix.session(projectId, sessionId).rescope({
  secrets: ['STRIPE_KEY'],
  connector_bindings: {
    'gmail-read': { connection_id: connection.connection_id },
  },
});
```

Each supplied field uses set semantics. The new value replaces the complete
previous value. Omit a field to leave it unchanged.

Connector changes apply to the next tool call. Secret removal stops future
delivery. It cannot remove a value from an existing model context or process.
Rotate the secret when prior disclosure matters.

## 5. Read session costs

The session-cost API combines finalized LLM cost and billed sandbox compute
cost. Every session appears in the list, including sessions with zero cost.

```bash
curl -sS \
  "$MACHINE_API_URL/usage/session-costs?project_id=$MACHINE_PROJECT_ID&limit=25&offset=0" \
  -H "Authorization: Bearer $MACHINE_API_KEY"

curl -sS \
  "$MACHINE_API_URL/usage/session-costs/$SESSION_ID?project_id=$MACHINE_PROJECT_ID" \
  -H "Authorization: Bearer $MACHINE_API_KEY"
```

The list returns session, project, owner, LLM, compute, total, request, token,
model, and compute-duration fields. It also returns `reconciliation` for
account usage that has no session.

The detail response adds:

- `model_usage`, grouped by provider and model
- `ledger_entries`, with discriminated `llm` and `compute` rows

Use the SDK for typed reads:

```ts
const page = await kortix.billing.sessionCosts.list({
  accountId,
  projectId,
  limit: 25,
  offset: 0,
});

const detail = await kortix.billing.sessionCosts.get(sessionId, {
  accountId,
  projectId,
});

const sameDetail = await kortix.session(projectId, sessionId).cost();
```

`session.cost()` does not start the session runtime.

## 6. Stream the answer

Await runtime readiness before using the OpenCode REST methods:

```ts
const handle = kortix.session(projectId, session.session_id);
await handle.ensureReady();

const stream = await handle.stream({
  onEvent: (event) => {
    // Render or persist the event.
  },
});

await handle.send('Summarize the support queue.');
```

Use `useSession(projectId, sessionId)` for React hosts. It owns startup,
readiness, the live event stream, and message synchronization.

## More endpoints

### Transcribe audio

`POST /v1/transcription` turns a recording into text. Send a multipart form
with the audio in a field named `audio_file`. The response is `{ "text": "…" }`.

```bash
curl -sS -X POST "$MACHINE_API_URL/transcription" \
  -H "Authorization: Bearer $MACHINE_API_KEY" \
  -F "audio_file=@recording.webm"
```

From the SDK, call `kortix.transcribe(file)` with a `File`.

The endpoint needs a signed-in caller: a sign-in token or an API key. Uploads
over 25 MB return `413`. A missing `audio_file` returns `400`. A deployment
with no speech-to-text provider configured returns `503`.

### Sync personal app state

`/v1/me/client-state` stores small app settings that follow one person across
their own devices. Machine Desktop and Machine Mobile use it for group chats
(`rooms`) and per-bot display settings (`bot-prefs`). Those are the only two
keys.

| Request | Result |
| --- | --- |
| `GET /v1/me/client-state` | `{ "items": { "rooms": { "value", "version", "updated_at" } } }` — one entry per saved key. |
| `PUT /v1/me/client-state/rooms` with `{ "value": { … }, "base_version": 3 }` | Saves the key and returns `{ "version": 4, "updated_at": "…" }`. |

`base_version` is the version your copy is based on. Send `0` for a key you
have never saved. A write wins only if nobody saved since that version.
Otherwise the API returns `409` with `{ "error": "conflict", "current": … }`,
which holds the saved copy. Merge your change into it and retry with its
version.

Each value must be a JSON object of 512 KB or less; a larger one returns
`413`. A deployment that has not created the storage table yet returns `503`;
keep the local copy and try later.

Only the person themselves can use these routes: their own sign-in, or their
own account-wide personal access token. A project-scoped token, a service
account, and the tokens agents carry inside a sandbox get `403`.

## Idempotent retries

Generate one `Idempotency-Key` for each logical session-create operation. Reuse
that key only when the request body is identical.

A replay with the same key and body returns the same session. A replay with a
different secret allowlist, connector binding map, or runtime context returns
`409`.

## Security rules

- The API derives session origin from the credential. The request body cannot
  select it.
- Connector credentials resolve server-side for each tool call.
- A connection must match its connector's authorization strategy.
- Connector policies apply to every connection under that connector.
- Project guardrails apply above connector-connection policies.
- Secret scope can narrow an agent grant. It cannot widen one.
- Session scope replacement cannot select a connection owned by another
  member.
- Store application customer metadata outside Machine.

## Common errors

| Status                       | Code                                             | Meaning                                                                            |
| ---------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `400`                        | `INVALID_SESSION_MODEL`                          | The selected model is not available to the account.                                |
| `400`                        | `INVALID_SESSION_CONNECTOR_BINDINGS`             | The binding map is malformed.                                                      |
| `400`                        | `INVALID_SESSION_RUNTIME_CONTEXT`                | Runtime context violates its shape, key, entry, or size limits.                    |
| `403`                        | `origin_override_forbidden`                      | A non-backend caller supplied a secret allowlist.                                  |
| `403`                        | `CONNECTOR_NOT_ASSIGNED`                         | The session runs the platform coordinator agent, which never gets connectors. Every project agent can use every connector. |
| `404` create / `403` rescope | `CONNECTOR_CONNECTION_NOT_FOUND`                    | The connection does not exist in this project or violates the connector's authorization strategy. |
| `404`                        | `SECRET_IDENTIFIER_NOT_FOUND`                    | The secret allowlist contains an unknown project-secret identifier.                |
| `409`                        | `CONNECTOR_CONNECTION_REQUIRED`               | A mandatory connector has no valid active connection. Every failing connector is listed in `connector_connections`. |
| `409`                        | `REQUIRED_CONNECTOR_CONNECTION_UNAVAILABLE`         | A required slug has no configured connector at all. Every failing alias is listed in `connectors`. |
| `409`                        | `CONNECTOR_NOT_PIPEDREAM`                        | The alias is a connector on the project but not a Pipedream one, so no Quick Connect link exists for it. |
| `409`                        | `CONNECTOR_PIPEDREAM_APP_MISSING`                | The Pipedream connector names no app, so no connect link can be built.  |
| `409` create / `403` rescope | `CONNECTOR_CONNECTION_INACTIVE`                     | The selected connection or connector is inactive.                       |
| `409`                        | `IDEMPOTENCY_*_CONFLICT`                         | The idempotency key was replayed with a different request body.                    |
| `402`                        | `subscription_required` / `insufficient_credits` | The account cannot start a billed session.                                         |
