> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.xapi.to/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.xapi.to/_mcp/server.

# REST API 调用流程

Sandbox 是有生命周期的计费资源，不是普通的一次性 API。一个完整调用必须包含
**资源选择、状态等待、业务执行、审计和清理**。仅收到创建或销毁操作的 HTTP 200/202，
不代表实例已经完成状态转换。

#### [全部接口与测试](/sandbox/api)

在一个页面查看并测试全部 17 个接口。

#### [CLI 安全封装](/sandbox/cli)

使用自动报价、等待、执行和 finally 清理的一次性命令。

#### [日志与审计](/sandbox/testing)

在 Console 中核对调用、状态、用量、账期与历史实例。

## 环境与鉴权

| 环境     | Base URL                       | 用途                           |
| ------ | ------------------------------ | ---------------------------- |
| 生产（默认） | `https://sandbox.xapi.to`      | 正式工作负载、文档 Explorer 与生成代码默认目标 |
| 测试     | `https://sandbox.test.xapi.to` | 集成测试、发布前验收；需要显式切换            |

所有需要账户身份的接口接受：

```http
XAPI-Key: sk-xapi-...
```

> **Warning**
>
> 测试 Gateway 仍会连接真实供应商、创建真实资源并从测试账户结算。不要把 API Key
> 写进 URL、源码、远端命令或截图；每次创建后都必须销毁实例。

## 完整生命周期

#### 查看规格与环境

调用 `GET /v1/offerings` 和 `GET /v1/environments`，确认资源、能力、暂停支持和价格。
这两个接口不会创建实例。

#### 按需求报价

调用 `POST /v1/quotes`，声明 `exec / files / ports`、CPU、内存、GPU、区域和
`maxEstimatedHourlyUsd`。保存返回的 `quoteId` 和 `expiresAt`。

#### 创建实例

调用 `POST /v1/sandboxes`，把 `quoteId` 放入 `selection`。为每个业务任务生成稳定的
`idempotencyKey`；如果创建响应丢失，重试必须复用同一个 key。

#### 等待 RUNNING

创建接口返回 `202 Accepted` 后，轮询 `GET /v1/sandboxes/{id}`。
只有 `observedState=RUNNING` 才能执行命令或访问文件。

#### 执行工作负载

根据 offering 能力调用 commands、files、ports 或 extensions。Shell 成功同时要求
HTTP 成功且响应中的 `exitCode=0`。

#### 检查审计

运行中可先读取审计定位问题，但此时 usage segment 和 billing period 通常仍开放，
不能作为最终结算证据。

#### 销毁并确认终态

在 `finally` 或 shell `trap` 中调用 `POST /v1/sandboxes/{id}/terminate`，随后继续轮询，
直到 `observedState=TERMINATED` 或 provider-terminal `FAILED`。进入终态后重新读取
`operations`、`events`、`usageSegments` 和 `billingPeriods`，核对 TERMINATE 操作、
终态事件、已关闭用量段、已结算账期和服务端 `totalCost`；最后用
`sandbox-history?state=ACTIVE` 确认本次实例没有残留。

## 在文档中试调

打开侧边栏的 **全部 REST API 与测试**。17 个接口按四类集中在同一页，组件内可以填写
参数、发送 Test 请求并查看完整响应。需要语言代码或完整 Schema 时再进入**逐接口 API Reference**。

#### 确认 Production 环境并鉴权

页面默认选择 `Production`；在鉴权框输入生产账户的 `XAPI-Key`。需要验收测试时再显式
切换到 `Test` 并改用测试账户 Key。不要把 Key 放进 URL 或请求体。

#### 报价并保存 quoteId

调用**获取 Sandbox 报价**。它不会创建资源，但会验证鉴权、预算和规格选择。

#### 创建并保存 instance id

把 `quoteId` 放入 `selection.quoteId`，使用稳定且唯一的 `idempotencyKey` 创建实例。

#### 等待 RUNNING 后执行

使用**查询实例详情**轮询；只有 `observedState=RUNNING` 后才调用命令、文件或端口接口。

#### 终止、等待和审计

无论中间是否成功，都调用**终止实例**，确认终态后查询审计与历史。

### 直接测试报价接口

下面是内嵌的真实请求构建器。它只生成报价，不创建实例：

### Request

POST [https://sandbox.xapi.to/v1/quotes](https://sandbox.xapi.to/v1/quotes)

```curl
curl -X POST https://sandbox.xapi.to/v1/quotes \
     -H "XAPI-Key: <apiKey>" \
     -H "Content-Type: application/json" \
     -d '{
  "requirements": {
    "cpu": {
      "min": 1
    },
    "memoryGiB": {
      "min": 1
    },
    "capabilities": [
      "exec",
      "files"
    ]
  },
  "maxEstimatedHourlyUsd": "0.20"
}'
```

```python
import requests

url = "https://sandbox.xapi.to/v1/quotes"

payload = {
    "requirements": {
        "cpu": { "min": 1 },
        "memoryGiB": { "min": 1 },
        "capabilities": ["exec", "files"]
    },
    "maxEstimatedHourlyUsd": "0.20"
}
headers = {
    "XAPI-Key": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://sandbox.xapi.to/v1/quotes';
const options = {
  method: 'POST',
  headers: {'XAPI-Key': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"requirements":{"cpu":{"min":1},"memoryGiB":{"min":1},"capabilities":["exec","files"]},"maxEstimatedHourlyUsd":"0.20"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://sandbox.xapi.to/v1/quotes"

	payload := strings.NewReader("{\n  \"requirements\": {\n    \"cpu\": {\n      \"min\": 1\n    },\n    \"memoryGiB\": {\n      \"min\": 1\n    },\n    \"capabilities\": [\n      \"exec\",\n      \"files\"\n    ]\n  },\n  \"maxEstimatedHourlyUsd\": \"0.20\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("XAPI-Key", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://sandbox.xapi.to/v1/quotes")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["XAPI-Key"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"requirements\": {\n    \"cpu\": {\n      \"min\": 1\n    },\n    \"memoryGiB\": {\n      \"min\": 1\n    },\n    \"capabilities\": [\n      \"exec\",\n      \"files\"\n    ]\n  },\n  \"maxEstimatedHourlyUsd\": \"0.20\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://sandbox.xapi.to/v1/quotes")
  .header("XAPI-Key", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"requirements\": {\n    \"cpu\": {\n      \"min\": 1\n    },\n    \"memoryGiB\": {\n      \"min\": 1\n    },\n    \"capabilities\": [\n      \"exec\",\n      \"files\"\n    ]\n  },\n  \"maxEstimatedHourlyUsd\": \"0.20\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://sandbox.xapi.to/v1/quotes', [
  'body' => '{
  "requirements": {
    "cpu": {
      "min": 1
    },
    "memoryGiB": {
      "min": 1
    },
    "capabilities": [
      "exec",
      "files"
    ]
  },
  "maxEstimatedHourlyUsd": "0.20"
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'XAPI-Key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://sandbox.xapi.to/v1/quotes");
var request = new RestRequest(Method.POST);
request.AddHeader("XAPI-Key", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"requirements\": {\n    \"cpu\": {\n      \"min\": 1\n    },\n    \"memoryGiB\": {\n      \"min\": 1\n    },\n    \"capabilities\": [\n      \"exec\",\n      \"files\"\n    ]\n  },\n  \"maxEstimatedHourlyUsd\": \"0.20\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "XAPI-Key": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "requirements": [
    "cpu": ["min": 1],
    "memoryGiB": ["min": 1],
    "capabilities": ["exec", "files"]
  ],
  "maxEstimatedHourlyUsd": "0.20"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://sandbox.xapi.to/v1/quotes")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

> **Tip**
>
> Explorer 会根据 OpenAPI 示例自动生成 curl、TypeScript、Python 等调用代码。页面不会
> 把你的 API Key写入仓库；但共享屏幕或录屏时仍应清空鉴权框。

## 一键测试脚本

下面的脚本默认连接生产 Gateway，验证 quote → create → wait → exec → audit → terminate。
需要测试环境时，显式把 `base_url` 改为 `https://sandbox.test.xapi.to` 并使用测试 Key。
它要求本机安装 `curl` 和 `jq`，并通过 `trap` 尽力清理实例。

#### 展开完整 curl 生命周期脚本

```bash
#!/usr/bin/env bash
set -euo pipefail

base_url="https://sandbox.xapi.to"
read -rsp "XAPI production key: " XAPI_KEY
printf '\n'

instance_id=""
cleanup() {
  if [[ -n "$instance_id" ]]; then
    curl -fsS -X POST "$base_url/v1/sandboxes/$instance_id/terminate" \
      -H "XAPI-Key: $XAPI_KEY" \
      -H "Content-Type: application/json" \
      -d "{\"idempotencyKey\":\"docs-terminate-$instance_id\"}" >/dev/null || true
  fi
}
trap cleanup EXIT INT TERM

quote_json="$(curl -fsS -X POST "$base_url/v1/quotes" \
  -H "XAPI-Key: $XAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "requirements": {"capabilities": ["exec", "files"]},
    "maxEstimatedHourlyUsd": "0.20"
  }')"
quote_id="$(jq -er '.quoteId' <<<"$quote_json")"

create_key="docs-create-$(date +%s)-$RANDOM"
create_json="$(curl -fsS -X POST "$base_url/v1/sandboxes" \
  -H "XAPI-Key: $XAPI_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"selection\": {\"quoteId\": \"$quote_id\"},
    \"metadata\": {\"client\": \"docs-curl\", \"purpose\": \"sandbox-api-test\"},
    \"idempotencyKey\": \"$create_key\",
    \"policy\": {\"resumeOnAccess\": false}
  }")"
instance_id="$(jq -er '.id' <<<"$create_json")"
printf 'instance=%s\n' "$instance_id"

for _ in $(seq 1 90); do
  detail="$(curl -fsS "$base_url/v1/sandboxes/$instance_id" \
    -H "XAPI-Key: $XAPI_KEY")"
  state="$(jq -r '.observedState' <<<"$detail")"
  [[ "$state" == "RUNNING" ]] && break
  [[ "$state" == "FAILED" || "$state" == "TERMINATED" ]] && {
    jq . <<<"$detail"; exit 1;
  }
  sleep 2
done
[[ "${state:-}" == "RUNNING" ]] || { echo "RUNNING timeout" >&2; exit 1; }

exec_json="$(curl -fsS -X POST "$base_url/v1/sandboxes/$instance_id/commands" \
  -H "XAPI-Key: $XAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"python3 -c \"print(6 * 7)\"","timeoutSeconds":60}')"
jq -e '.exitCode == 0 and (.stdout | contains("42"))' <<<"$exec_json" >/dev/null

cleanup
trap - EXIT INT TERM

for _ in $(seq 1 90); do
  final_json="$(curl -fsS "$base_url/v1/sandboxes/$instance_id" \
    -H "XAPI-Key: $XAPI_KEY")"
  final_state="$(jq -r '.observedState' <<<"$final_json")"
  [[ "$final_state" == "TERMINATED" || "$final_state" == "FAILED" ]] && break
  sleep 2
done
jq '{id,observedState,totalCost,auditCounts}' <<<"$final_json"
[[ "$final_state" == "TERMINATED" || "$final_state" == "FAILED" ]]

# 终态之后再读取审计；此前的用量段/账期仍可能是开放状态，不能作为结算证据。
for _ in $(seq 1 30); do
  operations="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=operations&page=1&pageSize=100" \
    -H "XAPI-Key: $XAPI_KEY")"
  events="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=events&page=1&pageSize=100" \
    -H "XAPI-Key: $XAPI_KEY")"
  usage="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=usageSegments&page=1&pageSize=100" \
    -H "XAPI-Key: $XAPI_KEY")"
  billing="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=billingPeriods&page=1&pageSize=100" \
    -H "XAPI-Key: $XAPI_KEY")"

  if jq -e 'any(.items[]; .type == "TERMINATE" and .status == "SUCCEEDED")' <<<"$operations" >/dev/null \
    && jq -e 'any(.items[]; .currentState == "TERMINATED" or .currentState == "FAILED")' <<<"$events" >/dev/null \
    && jq -e '(.total > 0) and all(.items[]; .endsAt != null and .status != "OPEN")' <<<"$usage" >/dev/null \
    && jq -e '(.total > 0) and all(.items[]; .endedAt != null and .status != "RESERVED")' <<<"$billing" >/dev/null; then
    break
  fi
  sleep 2
done

jq -e 'any(.items[]; .type == "TERMINATE" and .status == "SUCCEEDED")' <<<"$operations" >/dev/null
jq -e 'any(.items[]; .currentState == "TERMINATED" or .currentState == "FAILED")' <<<"$events" >/dev/null
jq -e '(.total > 0) and all(.items[]; .endsAt != null and .status != "OPEN")' <<<"$usage" >/dev/null
jq -e '(.total > 0) and all(.items[]; .endedAt != null and .status != "RESERVED")' <<<"$billing" >/dev/null

active_json="$(curl -fsS "$base_url/v1/sandbox-history?state=ACTIVE&page=1&pageSize=100" \
  -H "XAPI-Key: $XAPI_KEY")"
jq -e --arg id "$instance_id" 'all(.items[]; .id != $id)' <<<"$active_json" >/dev/null
printf 'audit and cleanup verified for %s; totalCost=%s\n' \
  "$instance_id" "$(jq -r '.totalCost' <<<"$final_json")"
```

## 推荐：CLI 安全封装

如果只是验证 Sandbox 是否能执行命令，优先使用 CLI。它已封装报价、等待和 finally 清理：

```bash
XAPI_SANDBOX_HOST=sandbox.xapi.to \
npx xapi-to sandbox run \
  --capabilities exec,files \
  --max-hourly-usd 0.20 \
  --command 'python3 -c "print(6 * 7)"' \
  --format pretty
```

成功条件不只是 stdout 包含 `42`，还应确认 `cleanup.state`、`finalState`、`totalCost`
以及 Console 中对应的操作和账期记录。

## 常见错误

| 状态    | 含义            | 处理                             |
| ----- | ------------- | ------------------------------ |
| `400` | 参数错误或没有兼容规格   | 重新查看 offerings，减少能力或调整预算       |
| `401` | API Key 缺失或无效 | 检查鉴权框/请求头，不要把 key 放进 URL       |
| `402` | 余额或密钥限额不足     | 在测试账户充值或调整密钥额度                 |
| `409` | 当前状态不允许操作     | 查询实例状态，等待正在执行的状态变更结束           |
| `429` | 创建频率过高        | 不要循环创建；复用相同 idempotencyKey 并退避 |

> **Info**
>
> 需要 OpenAI Agents SDK + DeepSeek 的组合流程，请继续阅读
> [AI + Sandbox Agent](/sandbox/openai-agents)。