API Reference

Sandbox/Thread runtime with AI SDK compatible streaming.

Sandboxes

A sandbox is a persistent E2B environment with its own filesystem, git state, and sessions. All endpoints require Authorization: Bearer <api_key> header.

Create Sandbox

POST https://relay.an.dev/v1/sandboxes

Required: agent (slug). Optional: files, envs, setup — these override/extend the agent's deployed sandbox config.

// POST /v1/sandboxes
{
  "agent": "my-agent",
  "files": { "/home/user/config.json": "{\"key\": \"value\"}" },
  "envs": { "API_TOKEN": "sk-..." },
  "setup": ["npm install -g prettier"]
}

// Response
{
  "id": "sb_abc123",
  "sandboxId": "e2b-sandbox-id",
  "status": "active",
  "createdAt": "2026-02-26T12:00:00Z"
}

Get Sandbox

GET https://relay.an.dev/v1/sandboxes/:id
// GET /v1/sandboxes/:id

// Response
{
  "id": "sb_abc123",
  "sandboxId": "e2b-sandbox-id",
  "status": "active",
  "error": null,
  "agent": { "slug": "my-agent", "name": "My Agent" },
  "threads": [
    { "id": "th_xyz789", "name": "Chat 1", "status": "completed" }
  ],
  "createdAt": "2026-02-26T12:00:00Z",
  "updatedAt": "2026-02-26T12:05:00Z"
}

Delete Sandbox

DELETE https://relay.an.dev/v1/sandboxes/:id
// DELETE /v1/sandboxes/:id

// Response: 204 No Content

Kills the E2B sandbox and cascades deletion to all threads within it.

Sandbox Operations

Run commands, read/write files, and clone repos inside a sandbox.

Execute Command

POST https://relay.an.dev/v1/sandboxes/:id/exec
// POST /v1/sandboxes/:id/exec
{
  "command": "ls -la /home/user",
  "cwd": "/home/user/repo",
  "envs": { "NODE_ENV": "production" },
  "timeoutMs": 30000
}

// Response
{
  "stdout": "total 24\ndrwxr-xr-x 5 user user 4096 ...",
  "stderr": "",
  "exitCode": 0
}

Write Files

POST https://relay.an.dev/v1/sandboxes/:id/files
// POST /v1/sandboxes/:id/files
{
  "files": [
    { "path": "/home/user/hello.txt", "content": "Hello world!" },
    { "path": "/home/user/config.json", "content": "{\"key\": \"value\"}" }
  ]
}

// Response: 200 OK

Read File

GET https://relay.an.dev/v1/sandboxes/:id/files?path=...
// GET /v1/sandboxes/:id/files?path=/home/user/hello.txt

// Response
{
  "path": "/home/user/hello.txt",
  "content": "Hello world!"
}

Clone Repository

POST https://relay.an.dev/v1/sandboxes/:id/git/clone
// POST /v1/sandboxes/:id/git/clone
{
  "url": "https://github.com/org/repo.git",
  "path": "/home/user/repo",
  "token": "ghp_...",
  "depth": 1
}

// Response
{
  "path": "/home/user/repo",
  "branch": "default"
}

For private repos, pass a token (e.g. GitHub PAT). Use depth: 1 for faster shallow clones.

Threads

A thread is a conversation within a sandbox. Each thread has its own message history, status, and cost tracking.

List Threads

GET https://relay.an.dev/v1/sandboxes/:id/threads
// GET /v1/sandboxes/:id/threads

// Response
[
  {
    "id": "th_xyz789",
    "name": "Chat 1",
    "status": "completed",
    "createdAt": "2026-02-26T12:00:00Z",
    "updatedAt": "2026-02-26T12:05:00Z"
  }
]

Create Thread

POST https://relay.an.dev/v1/sandboxes/:id/threads
// POST /v1/sandboxes/:id/threads
{
  "name": "Chat 1"
}

// Response
{
  "id": "th_xyz789",
  "name": "Chat 1",
  "status": "active",
  "createdAt": "2026-02-26T12:00:00Z"
}

Get Thread

GET https://relay.an.dev/v1/sandboxes/:id/threads/:threadId
// GET /v1/sandboxes/:id/threads/:threadId

// Response
{
  "id": "th_xyz789",
  "name": "Chat 1",
  "status": "completed",
  "messages": [
    { "role": "user", "content": "Hello" },
    { "role": "assistant", "content": "Hi there!" }
  ],
  "createdAt": "2026-02-26T12:00:00Z",
  "updatedAt": "2026-02-26T12:05:00Z"
}

Delete Thread

DELETE https://relay.an.dev/v1/sandboxes/:id/threads/:threadId
// DELETE /v1/sandboxes/:id/threads/:threadId

// Response: 204 No Content

Chat Endpoint

POST https://relay.an.dev/v1/chat/:slug

Body `sandboxId` + `threadId`: continue an existing thread in a sandbox.

Body `sandboxId` only: creates a new thread in the existing sandbox.

No sandbox/thread: creates a new sandbox and a new thread automatically.

`threadId` without `sandboxId`: returns a 400 error.

Resuming & Cancelling

GET  https://relay.an.dev/v1/chat/:slug/:sandboxId/stream
DELETE https://relay.an.dev/v1/chat/:slug/:sandboxId/stream

Use the sandbox ID in the URL to resume or cancel an active stream. Use DELETE .../stream for deterministic server-side cancel.

Request Body

{
  "sandboxId": "uuid-of-existing-sandbox",
  "threadId": "uuid-of-existing-thread",
  "messages": [
    {
      "id": "msg-1",
      "role": "user",
      "parts": [{ "type": "text", "text": "Your message" }]
    }
  ]
}

SSE Response

data: {"type":"start"}
data: {"type":"text-start","id":"..."}
data: {"type":"text-delta","id":"...","delta":"Hello world"}
data: {"type":"text-end","id":"..."}
data: {"type":"message-metadata","messageMetadata":{
  "sessionId":"...",
  "totalCostUsd":0.034,
  "inputTokens":3,
  "outputTokens":5,
  "durationMs":2487
}}
data: {"type":"finish"}
data: [DONE]

cURL

curl -N -X POST https://relay.an.dev/v1/chat/YOUR_AGENT_SLUG \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sandboxId": "uuid-of-existing-sandbox",
    "threadId": "uuid-of-existing-thread",
    "messages": [{
      "id": "msg-1",
      "role": "user",
      "parts": [{ "type": "text", "text": "Hello" }]
    }]
  }'

React (SDK)

For the recommended approach using @21st-sdk/nextjs with server-side token exchange, see the Get Started guide.

Server SDK

Runtime SDK examples for sandboxes, threads, runs, files, git, and tokens live on the Server SDK page.

React (raw AI SDK)

Direct usage with AI SDK — useful for custom transports or non-Next.js frameworks.

agent-chat.tsx
import { Chat, DefaultChatTransport } from "ai"
import { useChat } from "@ai-sdk/react"

const chat = new Chat({
  transport: new DefaultChatTransport({
    api: "https://relay.an.dev/v1/chat/YOUR_AGENT_SLUG",
    headers: async () => ({
      Authorization: "Bearer YOUR_API_KEY",
    }),
    body: {
      sandboxId: "uuid-of-existing-sandbox",  // optional
      threadId: "uuid-of-existing-thread",     // optional
    },
  }),
})

export function AgentChat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({ chat })

  return (
    <form onSubmit={handleSubmit}>
      {messages.map((m) => <div key={m.id}>{m.content}</div>)}
      <input value={input} onChange={handleInputChange} />
    </form>
  )
}
API Reference — 21st Agents SDK Docs