# API reference

> Base URL `https://api.spin.inductionlabs.com/v1`. Hosted Spin is `https://api.spin.inductionlabs.com/v1`. The machine-readable spec is at [openapi.json](https://api.spin.inductionlabs.com/openapi.json).

Rendered version: https://spin.inductionlabs.com/docs/api

## Authentication

Every request carries a bearer token. The token’s prefix decides what it can do.

| Scope | Token | Where it lives |
| --- | --- | --- |
| Account key | `cnv_live_…`, created in the dashboard | Host or CI |
| Signed-in session | Supabase access token from the dashboard | Browser |
| Wallet token | `spin_agent_…`, returned when a wallet is created | That agent’s sandbox |

```sh
curl "https://api.spin.inductionlabs.com/v1/account" -H "Authorization: Bearer $SPIN_API_KEY"
```

## Conventions

Account amounts are integer USD cents. Agent-side amounts are exact decimal USD strings. Lists return the latest 100 rows. Paid operations require a stable `Idempotency-Key` header: retry with the same key and body, and never retry an ambiguous paid operation under a new key.

Errors share one shape and a stable code. `402` insufficient funds, `403` wrong token scope, `409` conflicting retry or pending cleanup, `503` retryable dependency failure.

| Field | Type | Notes |
| --- | --- | --- |
| `error` | `object` | Fields: `code` (string); `message` (string). |
| `request_id` | `string` |  |

## Account endpoints

Called from your host or CI with an account key, or by the dashboard with a signed-in session. They allocate and settle wallets and never expose resource credentials except the wallet token itself.

### GET /v1/account

Account credit balance in USD cents

_Auth: Account key · Returns 200_

#### Response · Account

| Field | Type | Notes |
| --- | --- | --- |
| `available_cents` | `integer` |  |
| `reserved_cents` | `integer` |  |
| `spent_cents` | `integer` |  |
| `welcome_credit_cents` | `integer` |  |

### GET /v1/keys

List account key metadata, never secrets

_Auth: Account key · Returns 200_

#### Response

JSON object.

### POST /v1/keys

Create an account key (Verified Supabase session or self-host operator only)

_Auth: Signed-in session · Returns 201_

#### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `name` (required) | `string` | 1–80 characters. |

#### Response

JSON object.

### DELETE /v1/keys/{id}

Revoke an account API key

_Auth: Signed-in session · id in the path · Returns 200_

#### Response

JSON object.

### GET /v1/wallets

List latest 100 owned wallets

_Auth: Account key · Returns 200_

#### Response

JSON object.

### POST /v1/wallets

Reserve credit and mint sandbox credentials

_Auth: Account key · Idempotency-Key header required · Returns 201_

#### Request body · WalletRequest

| Field | Type | Notes |
| --- | --- | --- |
| `name` (required) | `string` | 1–80 characters. |
| `budget_cents` (required) | `integer` | 100–1000000. |
| `expires_in_seconds` | `integer` | Seconds until the wallet auto-closes (60..2592000). 0 means the wallet never expires; only closing settles it. 0–2592000. default 86400. |

#### Response · Allocation

| Field | Type | Notes |
| --- | --- | --- |
| `id` | `string` |  |
| `status` | `string` |  |
| `agent_token` | `string` | Sensitive wallet credential. Never log it. |
| `env` | `object` | Fields: `SPIN_BROKER_URL` (string (uri)); `SPIN_BROKER_TOKEN` (string): Same scoped credential as agent_token; inject only into its sandbox. |

### GET /v1/wallets/{id}

Read an owned wallet, including original budget and used budget after closure

_Auth: Account key · id in the path · Returns 200_

#### Response

JSON object.

### GET /v1/resources

List resource summaries across owned wallets, newest first; no resource credentials

_Auth: Account key · Returns 200_

#### Query parameters

| Name | Type | Notes |
| --- | --- | --- |
| `wallet_id` | `string` | Filter to an owned wallet, including closed wallets. 0–200 characters. |
| `before` | `string` | The previous response next_cursor; pages contain up to 100 resources. Empty next_cursor means the last page. 0–200 characters. |

#### Response

JSON object.

### GET /v1/wallets/{id}/token

Retrieve the existing token for an owned, active wallet; closed or expired wallets return 409

_Auth: Account key · id in the path · Returns 200_

#### Response · WalletToken

| Field | Type | Notes |
| --- | --- | --- |
| `agent_token` (required) | `string` | Existing scoped credential for an active, unexpired wallet. Never log it. |

### POST /v1/wallets/{id}/close

Freeze spending, clean up resources, settle, return unused credits

_Auth: Account key · id in the path · Returns 200_

#### Response

JSON object.

### GET /v1/wallets/{id}/resources

List resources belonging to an owned wallet

_Auth: Account key · id in the path · Returns 200_

#### Response

JSON object.

### POST /v1/billing/checkout

Create a Stripe Checkout session (browser session required)

_Auth: Signed-in session · Idempotency-Key header required · Returns 200_

#### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `amount_cents` (required) | `integer` | 500–100000. |

#### Response

JSON object.

## Agent endpoints

Called from inside a sandbox with that wallet’s token. Spending is capped by the wallet budget and expiry; paid operations reserve their maximum price first and settle afterwards.

### GET /v1/budget

Inspect wallet spending (decimal USD strings)

_Auth: Wallet token · Returns 200_

#### Response

JSON object.

### GET /v1/compute/offers

List approved Modal, SF Compute and Vast.ai offers

_Auth: Wallet token · Returns 200_

#### Response

JSON object.

### GET /v1/compute/list

List wallet-owned resources

_Auth: Wallet token · Returns 200_

#### Response

JSON object.

### GET /v1/models

List metered OpenRouter models

_Auth: Wallet token · Returns 200_

#### Response

JSON object.

### POST /v1/compute/create

Prepay a fixed-duration compute lease; no early-release refund

_Auth: Wallet token · Idempotency-Key header required · Returns 200_

#### Request body · ComputeRequest

| Field | Type | Notes |
| --- | --- | --- |
| `provider` (required) | `"modal" \| "sfcompute" \| "vastai"` |  |
| `spec` (required) | `object` | Provider-specific spec; GET /v1/compute/offers first. Modal: gpu,gpus,cpus,memory_gib,max_seconds. SF: chip,max_seconds. Vast: offer_id,disk_gb,max_seconds; stop destroys the rental and disk. |

#### Response · ComputeResource

| Field | Type | Notes |
| --- | --- | --- |
| `id` | `string` | Spin resource ID for subsequent requests. |
| `provider_id` | `string \| null` |  |
| `provider` | `string` |  |
| `status` | `string` |  |
| `billing_state` | `"reserved" \| "uncertain" \| "settled"` | Payment state; settled resources may still be running and need cleanup. |
| `billing_policy` | `"prepaid_time"` |  |
| `charged_usd` | `string` | Cumulative prepaid charge, including confirmed extensions; early release does not refund it. |
| `reserved_usd` | `string` | Unresolved creation hold; zero after capture. Check /v1/budget for pending extension holds. |
| `expires_at` | `number` | Current deadline, Unix seconds. |
| `purchased_seconds` | `integer` |  |
| `price_hourly_usd` | `string` |  |
| `supports_extension` | `boolean` |  |

### POST /v1/compute/extend

Prepay additional seconds on an active SF or Vast lease; Modal returns extension_not_supported

_Auth: Wallet token · Idempotency-Key header required · Returns 200_

#### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `id` (required) | `string` |  |
| `additional_seconds` (required) | `integer` | Added to the current expiry; constrained by wallet expiry, remaining credit, and provider limits. 1–86400. |

#### Response · ComputeResource

| Field | Type | Notes |
| --- | --- | --- |
| `id` | `string` | Spin resource ID for subsequent requests. |
| `provider_id` | `string \| null` |  |
| `provider` | `string` |  |
| `status` | `string` |  |
| `billing_state` | `"reserved" \| "uncertain" \| "settled"` | Payment state; settled resources may still be running and need cleanup. |
| `billing_policy` | `"prepaid_time"` |  |
| `charged_usd` | `string` | Cumulative prepaid charge, including confirmed extensions; early release does not refund it. |
| `reserved_usd` | `string` | Unresolved creation hold; zero after capture. Check /v1/budget for pending extension holds. |
| `expires_at` | `number` | Current deadline, Unix seconds. |
| `purchased_seconds` | `integer` |  |
| `price_hourly_usd` | `string` |  |
| `supports_extension` | `boolean` |  |

### POST /v1/run/close

Freeze wallet spending and clean up agent resources

_Auth: Wallet token · Returns 200_

#### Response

JSON object.

### POST /v1/compute/stop

Stop an owned resource

_Auth: Wallet token · Returns 200_

#### Request body · ResourceID

| Field | Type | Notes |
| --- | --- | --- |
| `id` (required) | `string` |  |

#### Response

JSON object.

### POST /v1/compute/ssh

Ssh an owned resource

_Auth: Wallet token · Returns 200_

#### Request body · ResourceID

| Field | Type | Notes |
| --- | --- | --- |
| `id` (required) | `string` |  |

#### Response

JSON object.

### POST /v1/compute/status

Status an owned resource

_Auth: Wallet token · Returns 200_

#### Request body · ResourceID

| Field | Type | Notes |
| --- | --- | --- |
| `id` (required) | `string` |  |

#### Response

JSON object.

### POST /v1/compute/exec

Execute inside an owned remote sandbox

_Auth: Wallet token · Returns 200_

#### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `id` (required) | `string` |  |
| `command` (required) | `string` |  |
| `timeout` | `integer` | 1–3600. default 300. |

#### Response

JSON object.

### POST /v1/compute/upload

Upload at most 8 MiB to an owned resource

_Auth: Wallet token · Returns 200_

#### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `id` (required) | `string` |  |
| `path` (required) | `string` |  |
| `content_base64` (required) | `string` |  |

#### Response

JSON object.

### POST /v1/transfer/sign

Sign an exact object URL when transfer storage is configured

_Auth: Wallet token · Returns 200_

#### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `object` (required) | `string` |  |
| `operation` (required) | `"get" \| "put"` |  |
| `expires_seconds` | `integer` | default 900. |

#### Response

JSON object.

### POST /v1/openrouter/key

Get wallet-scoped key and OpenAI-compatible base_url

_Auth: Wallet token · Returns 200_

#### Request body

Free-form JSON object.

#### Response

JSON object.

### POST /v1/chat/completions

Metered non-streaming OpenRouter request

_Auth: Wallet token · Idempotency-Key header required · Returns 200_

#### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `model` (required) | `string` |  |
| `messages` (required) | `object[]` |  |
| `max_tokens` | `integer` | min 1. |
| `stream` | `false` |  |

#### Response

JSON object.

## Also available

- [Documentation (Markdown)](https://spin.inductionlabs.com/docs.md)
- [OpenAPI specification](https://api.spin.inductionlabs.com/openapi.json)
