REST API
概述
Arrival Space REST API 允许第三方应用程序验证用户身份、通过预签名 URL 上传资源、跟踪异步处理任务、创建和管理空间、管理实体以及配置传送门。
功能
- 使用预签名 S3 URL 上传资源并确认处理
- 通过实时进度跟踪异步处理任务
- 从上传创建空间并更新隐私设置
- 创建、读取、更新和删除空间中的实体
- 使用实体端点配置静态和动态传送门
在开始上传之前,请确保您有足够的可用存储空间。否则,上传可能会失败。
基础 URL 与版本
const API_BASE_URL = "https://api-staging.arrival.space/api";
const VERSION = "v1";
所有端点应以 ${API_BASE_URL}/${VERSION} 为前缀。
API 端点将来可能会更改。如果您遇到任何连接问题,请联系我们的支持团队。
认证
所有 API 请求需要在 Authorization 标头中携带 Bearer 令牌:
Authorization: Bearer <api_key>
选项 1:用户账户 API 密钥
- 登录 Arrival Space
- 导航到您的账户设置
- 生成或复制您的 API 密钥
选项 2:OAuth 2.0 登录(用于原生应用)
原生应用程序可以使用带 PKCE 的 OAuth 2.0 授权码流程让用户通过其 Arrival.Space 账户(Google 登录或邮箱/密码)登录。这是推荐的第三方集成方法,用户无需手动复制 API 密钥。
OAuth 流程使用标准端点,并返回 API 密钥作为访问令牌 — 与用户账户设置中显示的密钥相同。这意味着令牌在服务器重启后仍然有效,并在所有 API 端点中以相同方式工作。
OAuth 端点:
| 端点 | 描述 |
|---|---|
GET /.well-known/oauth-authorization-server | OAuth 2.0 服务器元数据(自动发现) |
POST /register | 动态客户端注册(RFC 7591) |
GET /authorize | 授权端点 — 重定向到登录页面 |
POST /token | 令牌交换 — 返回访问令牌 |
POST /revoke | 令牌撤销 |
流程概述:
-
注册您的客户端(一次性,或在首次启动时):
const regResponse = await fetch(`${API_BASE_URL}/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_name: "My App",
redirect_uris: ["https://myapp.example.com/callback"],
grant_types: ["authorization_code"],
response_types: ["code"],
token_endpoint_auth_method: "none"
})
});
const client = await regResponse.json();
// Save client.client_id for future use -
生成 PKCE 挑战并引导用户进行授权:
// Generate PKCE code verifier + challenge
const codeVerifier = generateRandomString(64);
const codeChallenge = base64url(sha256(codeVerifier));
// Open browser to:
const authUrl = new URL(`${API_BASE_URL}/authorize`);
authUrl.searchParams.set("client_id", client.client_id);
authUrl.searchParams.set("redirect_uri", "https://myapp.example.com/callback");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set("state", generateRandomString(32));用户将看到 Arrival.Space 登录页面,包含 Google 登录和邮箱/密码选项。
-
处理回调 — 登录后,浏览器会重定向到您的
redirect_uri,带有?code=...&state=...:// Your callback handler receives: code, state
const tokenResponse = await fetch(`${API_BASE_URL}/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: "https://myapp.example.com/callback",
client_id: client.client_id,
code_verifier: codeVerifier
})
});
const tokens = await tokenResponse.json();
const apiKey = tokens.access_token; -
使用令牌 — 它是标准 API 密钥,使用方式与其他密钥相同:
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces`, {
headers: { Authorization: `Bearer ${apiKey}` }
});
- OAuth 流程返回的
access_token是用户的实际 API 密钥 — 与其账户设置中显示的相同 - 令牌不会过期(没有
expires_in限制)— 在用户撤销密钥之前持续有效 - 由于访问令牌不会过期,因此不需要刷新令牌
- 如果用户已有 API 密钥,将返回现有密钥(不会创建重复项)
如果您的服务器位于反向代理(Apache、nginx)之后,请确保 /.well-known/oauth-authorization-server 被转发到 Node.js 后端。如果发现功能不可用,客户端可以直接使用端点:/authorize、/token、/register。
OAuth 端点也兼容 Model Context Protocol (MCP)。Claude Desktop 等 MCP 客户端可以使用 https://api-dev.arrival.space/api/v1/mcp 直接连接 — OAuth 流程由 MCP 客户端自动处理。
约定
- 响应使用通用封装格式,包含
status、data,有时还有message或error - 时间戳为 ISO 8601 字符串(例如
2025-01-15T10:30:00.000Z) - 某些操作是异步的,返回一个
job_id,您需要通过GET /jobs/:jobId轮询,直到job_status为completed或failed resource_key标识已上传的资源,用于创建空间或实体spaceId是空间 URL 中的唯一标识符(例如https://arrival.space/12345678_9012)entity_id标识空间中的特定实体
速率限制
限制按 API 密钥执行(如果未提供密钥则回退到 IP)。429 响应包含 Retry-After 标头和响应体中的 retry_after_seconds 字段。
| 范围 | 限制 |
|---|---|
| 所有 API 端点 | 300 次请求/分钟 |
POST /files/upload | 20 次请求/分钟 |
POST /files/upload-complete | 20 次请求/分钟 |
GET /jobs/:jobId | 120 次请求/分钟 |
POST /user/create-space | 60 次请求/分钟 |
POST /spaces/update-privacy | 60 次请求/分钟 |
实体 CRUD(/spaces/:spaceId/entities*) | 300 次请求/分钟 |
快速入门
流程概要
- 获取 API 密钥(用户账户密钥或 OAuth 登录)
- 请求预签名上传 URL
- 将文件上传到 S3
- 确认上传,如需要则轮询异步处理完成状态
- 创建空间并管理实体和传送门
完整集成示例
const API_BASE_URL = "https://api-staging.arrival.space/api";
const VERSION = "v1";
/**
* Poll job status until completion or failure
* Used for async operations like zip file processing or large file copying
* Provides real-time progress updates during processing
*/
async function pollJobStatus(apiKey, jobId, onProgress = null) {
const pollUrl = `${API_BASE_URL}/${VERSION}/jobs/${jobId}`;
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const res = await fetch(pollUrl, {
headers: { Authorization: `Bearer ${apiKey}` }
});
const data = await res.json();
const job = data.data;
// Optional progress callback
if (onProgress) {
onProgress(job.progress, job.message);
}
if (job.job_status === "completed") {
resolve(job.result.resource_key);
} else if (job.job_status === "failed") {
reject(new Error(job.error || "Job failed"));
} else {
// Still processing, poll again in 2 seconds
setTimeout(poll, 2000);
}
} catch (e) {
reject(e);
}
};
poll();
});
}
/**
* Upload a file to Arrival Space and create a space
*/
async function uploadFileToArrivalSpace(apiKey, file, spaceTitle, spaceDescription = "") {
// Step 1: Request presigned upload URL
const uploadRequest = await fetch(`${API_BASE_URL}/${VERSION}/files/upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
file_name: file.name,
file_size: file.size,
content_type: file.type || "application/octet-stream"
})
});
if (!uploadRequest.ok) {
throw new Error("Failed to get upload URL");
}
const { data } = await uploadRequest.json();
const { params } = data;
// Step 2: Upload file to S3
const s3Upload = await fetch(params.url, {
method: params.method,
body: file,
headers: params.headers
});
if (!s3Upload.ok) {
throw new Error("Failed to upload file to S3");
}
// Step 3: Confirm upload
const fileUrl = params.url.split("?")[0]; // Remove query params
const confirmRequest = await fetch(`${API_BASE_URL}/${VERSION}/files/upload-complete`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
status: "success",
extra_info: {
file_url: fileUrl
}
})
});
if (!confirmRequest.ok) {
throw new Error("Failed to confirm upload");
}
const confirmData = await confirmRequest.json();
let resource_key;
// Step 3b: Handle async processing for large files (zip files or files over 1.5GB)
if (confirmData.status === "processing" && confirmData.data.job_id) {
console.log("Large file detected - polling for completion...");
resource_key = await pollJobStatus(apiKey, confirmData.data.job_id, (progress, message) => {
console.log(`Processing: ${progress}% - ${message}`);
});
} else {
// Immediate response for small files (under 1.5GB, non-zip)
resource_key = confirmData.data.resource_key;
}
// Step 4: Create space with uploaded file
const createSpaceRequest = await fetch(`${API_BASE_URL}/${VERSION}/user/create-space`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
space_data: {
title: spaceTitle,
description: spaceDescription,
resource_key: resource_key,
}
})
});
if (!createSpaceRequest.ok) {
throw new Error("Failed to create space");
}
return await createSpaceRequest.json();
}
// Usage
const apiKey = "your_api_key_here";
const file = document.getElementById("fileInput").files[0];
uploadFileToArrivalSpace(apiKey, file, "My New Space", "Optional description")
.then(result => console.log("Space created:", result.data))
.catch(error => console.error("Upload failed:", error));
API 参考
上传
请求上传 URL
POST /files/upload
生成预签名 S3 URL 用于文件上传。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
Content-Type | application/json | 是 | JSON 请求体 |
请求体参数:
| 字段 | 类型 | 必需 | 描述 |
|---|---|---|---|
file_name | string | 是 | 文件名 |
file_size | number | 是 | 文件大小(字节) |
content_type | string | 是 | MIME 类型 |
响应
{
"status": "ok",
"data": {
"upload_type": "presigned_url",
"params": {
"url": "https://s3.amazonaws.com/bucket/...?X-Amz-Signature=...",
"method": "PUT",
"headers": { "Content-Type": "application/zip" }
}
}
}
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/files/upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
file_name: "example.zip",
file_size: 1048576000,
content_type: "application/zip"
})
});
上传到 S3
收到预签名 URL 后,使用返回的参数将文件直接上传到 S3:
fetch(params.url, {
method: params.method,
headers: params.headers,
body: file,
});
确认上传
POST /files/upload-complete
在文件成功上传到 S3 后调用此端点。确认上传完成并返回资源密钥,您可以稍后用于创建空间或其他实体。
对于 .zip 文件或超过 1.5GB 的文件,此端点返回任务 ID 而非直接返回资源密钥。您必须轮询任务状态端点以在处理完成后获取最终的 resource_key。详见获取任务状态。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
Content-Type | application/json | 是 | JSON 请求体 |
请求体参数:
| 字段 | 类型 | 必需 | 描述 |
|---|---|---|---|
status | string | 是 | 上传完成后使用 "success" |
extra_info.file_url | string | 是 | S3 中已上传文件的 URL |
响应(小文件)
对于 1.5GB 以下的非 zip 文件,立即返回资源密钥:
{
"status": "confirmed",
"data": {
"resource_key": "api-uploads/...ply"
}
}
响应(大文件 - 异步处理)
对于 .zip 文件或超过 1.5GB 的文件,返回用于轮询的任务 ID:
{
"status": "processing",
"message": "Large file queued for processing",
"data": {
"job_id": "a1b2c3d4e5f6...",
"poll_url": "/api/v1/jobs/a1b2c3d4e5f6..."
}
}
| 字段 | 描述 |
|---|---|
status | "processing" 表示异步任务已启动 |
job_id | 用于轮询任务状态的唯一标识符 |
poll_url | 便捷轮询 URL |
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/files/upload-complete`, {
method: "POST",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
status: "success",
extra_info: {
file_url: "https://s3.amazonaws.com/bucket/uploads/example.zip"
}
})
});
任务
获取任务状态
GET /jobs/:jobId
轮询此端点以检查异步任务的状态(例如 zip 文件处理或大文件复制)。该端点提供实时进度更新。持续轮询直到 job_status 为 "completed" 或 "failed"。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
URL 参数:
| 参数 | 类型 | 必需 | 描述 |
|---|---|---|---|
jobId | string | 是 | upload-complete 返回的任务 ID |
响应
{
"status": "ok",
"data": {
"job_id": "a1b2c3d4e5f6...",
"job_status": "processing",
"progress": 45,
"message": "Copying file... 45% (2.3 GB / 5.0 GB)",
"result": null,
"error": null,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:31:30.000Z"
}
}
已完成任务示例:
{
"status": "ok",
"data": {
"job_id": "a1b2c3d4e5f6...",
"job_status": "completed",
"progress": 100,
"message": "File copied successfully",
"result": {
"resource_key": "api-uploads/..."
},
"error": null,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:32:15.000Z"
}
}
任务状态值
| 状态 | 描述 |
|---|---|
queued | 任务等待处理中 |
processing | 任务正在处理中 |
completed | 任务成功完成 — result.resource_key 可用 |
failed | 任务失败 — 检查 error 字段获取详情 |
响应字段
| 字段 | 类型 | 描述 |
|---|---|---|
job_id | string | 唯一任务标识符 |
job_status | string | 任务当前状态 |
progress | number | 进度百分比(0-100),处理期间实时更新 |
message | string | 人类可读的状态消息,包含进度详情(例如 "Copying file... 45% (2.3 GB / 5.0 GB)") |
result | object | 完成时的结果数据(包含 resource_key) |
error | string | 任务失败时的错误消息 |
created_at | string | 任务创建的 ISO 时间戳 |
updated_at | string | 最后状态更新的 ISO 时间戳 |
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/jobs/${jobId}`, {
headers: { Authorization: `Bearer ${api_key}` }
});
对于 zip 文件和大文件(超过 1.5GB),progress 和 message 字段在文件处理过程中实时更新。您可以每 1-2 秒轮询此端点以获取实时进度更新,显示:
- 完成百分比
- 已处理数据量(MB 或 GB)
- 当前操作(例如 "Copying file..."、"Extracting zip..."、"Uploading files...")
示例:轮询任务完成状态
async function pollJobStatus(apiKey, jobId) {
const pollUrl = `${API_BASE_URL}/${VERSION}/jobs/${jobId}`;
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const res = await fetch(pollUrl, {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await res.json();
const job = data.data;
console.log(`Progress: ${job.progress}% - ${job.message}`);
if (job.job_status === 'completed') {
resolve(job.result.resource_key);
} else if (job.job_status === 'failed') {
reject(new Error(job.error));
} else {
// Still processing, poll again in 2 seconds
setTimeout(poll, 2000);
}
} catch (e) {
reject(e);
}
};
poll();
});
}
列出任务
GET /jobs
列出已认证用户的所有任务。适用于跟踪多个异步操作。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
响应
{
"status": "ok",
"data": {
"jobs": [
{
"job_id": "a1b2c3d4e5f6...",
"type": "zip-processing",
"job_status": "completed",
"progress": 100,
"message": "Zip file processed successfully",
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:32:15.000Z"
},
{
"job_id": "b2c3d4e5f6a1...",
"type": "file-processing",
"job_status": "processing",
"progress": 65,
"message": "Copying file... 65% (3.25 GB / 5.0 GB)",
"created_at": "2025-01-15T11:00:00.000Z",
"updated_at": "2025-01-15T11:01:30.000Z"
}
],
"count": 1
}
}
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/jobs`, {
headers: { Authorization: `Bearer ${api_key}` }
});
任务会在 1 小时后自动清理。
空间
创建空间
POST /user/create-space
使用已上传文件的密钥在 Arrival Space 中创建新空间。在确认上传后调用此端点。
空间类型
空间有两种类型:
| 类型 | 描述 |
|---|---|
"infinite" | 默认。 没有房间架构的开放空间。3D 内容悬浮在开放环境中。不创建静态传送门框架。最适合展示单个 3D 模型。 |
"hub" | 带有架构的房间 — 墙壁、地板、天花板,以及房间周围排列的 7 个静态传送门框架。最适合创建带有通往其他空间传送门的主空间。 |
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
Content-Type | application/json | 是 | JSON 请求体 |
请求体参数:
| 参数 | 类型 | 必需 | 描述 |
|---|---|---|---|
title | string | 否 | 空间标题(省略时从文件名自动检测) |
description | string | 否 | 空间描述 |
resource_key | string | 否 | 来自 upload-complete 的资源密钥。省略时创建空空间 |
space_type | string | 否 | "hub" 或 "infinite"。默认:"infinite" |
响应
{
"status": "ok",
"message": "Space created successfully",
"data": {
"space_url": "https://arrival.space/12345678_9012",
"title": "My New Space",
"space_type": "hub"
}
}
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/user/create-space`, {
method: "POST",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
space_data: {
title: "My New Space",
description: "Optional description",
resource_key: "api-uploads/...ply",
space_type: "hub"
}
})
});
更新空间隐私设置
POST /spaces/update-privacy
更新现有空间的隐私设置。需要空间所有权。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
Content-Type | application/json | 是 | JSON 请求体 |
请求体参数:
| 参数 | 类型 | 必需 | 描述 |
|---|---|---|---|
spaceId | string | 是 | 空间的唯一标识符(在空间 URL 中找到) |
roomPrivacy | string | 是 | 隐私设置。可选值:"Public"、"Private"、"Link Only" |
informFollowers | boolean | 否 | 如果为 true,当空间设为"Public"时会通知关注者 |
隐私选项
| 选项 | 描述 |
|---|---|
Public | 空间公开可见且可被发现。可以通知关注者。 |
Private | 空间私有且不可被发现。仅所有者可通过直接链接访问。 |
Link Only | 空间可通过直接链接访问但不会公开列出。需要 Pro 订阅。 |
响应
成功:
{
"status": "ok",
"message": "Space privacy updated successfully"
}
错误响应:
// 404 - Space not found
{
"status": "error",
"message": "Space not found"
}
// 403 - Not authorized
{
"status": "error",
"message": "You are not authorized to update the privacy of this space"
}
// 400 - Invalid privacy option
{
"status": "error",
"message": "Invalid privacy option"
}
// 400 - Pro required for Link Only
{
"status": "error",
"message": "You need to be a Pro user to set a space to Link Only"
}
请求示例
async function updateSpacePrivacy(apiKey, spaceId, privacy, informFollowers = false) {
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/update-privacy`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
spaceId: spaceId,
roomPrivacy: privacy,
informFollowers: informFollowers
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return await response.json();
}
// Usage
updateSpacePrivacy("your_api_key", "12345678_9012", "Public", true)
.then(result => console.log("Privacy updated:", result))
.catch(error => console.error("Update failed:", error));
实体
实体是空间中的对象(3D 模型、传送门、媒体等)。这些端点允许您在自己拥有的空间中创建、读取、更新和删除实体。
所有实体端点需要 Authorization: Bearer <api_key> 标头和空间所有权。
创建实体
POST /spaces/:spaceId/entities
在空间中创建新实体。提供 resource_key 时,实体从已上传文件创建,并自动检测默认值(根据文件类型确定缩放、旋转)。省略时创建空白实体。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
Content-Type | application/json | 是 | JSON 请求体 |
请求体参数:
| 参数 | 类型 | 必需 | 描述 |
|---|---|---|---|
resource_key | string | 否 | 来自 upload-complete 的 S3 密钥。触发基于文件的实体创建,自动检测缩放/旋转 |
entity_id | string | 否 | 自定义实体 ID。省略时自动生成 |
entity_type | string | 否 | 实体类型(例如 "UserModelEntity"、"Simple"、"Gate") |
entity_data | object | 否 | 实体数据。提供 resource_key 时与自动检测的默认值合并 |
响应 (201):
{
"status": "ok",
"data": {
"entity_id": "user-model-91de72809185c9fa",
"entity_type": "UserModelEntity",
"entity_data": {
"glbUrl": "https://s3.amazonaws.com/...",
"scale": [2, 2, 2],
"rotation": { "x": 0, "y": 0, "z": 0 },
"position": [0, 1, 0]
}
}
}
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities`, {
method: "POST",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
resource_key: "api-uploads/...glb",
entity_id: "my-custom-id",
entity_type: "UserModelEntity",
entity_data: {
scale: [2, 2, 2],
position: [0, 1, 0]
}
})
});
列出实体
GET /spaces/:spaceId/entities
返回空间中的所有实体(不包括内部 RoomInfo 实体)。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
查询参数:
| 参数 | 类型 | 必需 | 描述 |
|---|---|---|---|
limit | number | 否 | 返回实体最大数量(默认 50,最大 200) |
cursor | string | 否 | 来自 nextCursor 的分页游标 |
响应:
{
"status": "ok",
"data": {
"entities": [
{
"entity_id": "user-model-91de72809185c9fa",
"entity_type": "UserModelEntity",
"entity_data": { "glbUrl": "...", "scale": 1, "rotation": { "x": 0, "y": 0, "z": 0 } },
"entity_state": {},
"created_date": "2026-01-27T12:00:00.000Z",
"change_date": "2026-01-27T12:00:00.000Z"
}
],
"count": 1,
"total": 1,
"nextCursor": null,
"hasMore": false
}
}
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities?limit=50`, {
headers: { Authorization: `Bearer ${api_key}` }
});
获取实体
GET /spaces/:spaceId/entities/:entityId
按 ID 返回单个实体。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
路径参数:
| 参数 | 类型 | 必需 | 描述 |
|---|---|---|---|
spaceId | string | 是 | 空间 ID(来自空间 URL) |
entityId | string | 是 | 实体 ID |
响应:
{
"status": "ok",
"data": {
"entity_id": "user-model-91de72809185c9fa",
"entity_type": "UserModelEntity",
"entity_data": { "glbUrl": "...", "scale": 1, "rotation": { "x": 0, "y": 0, "z": 0 } },
"entity_state": {},
"created_date": "2026-01-27T12:00:00.000Z",
"change_date": "2026-01-27T12:00:00.000Z"
}
}
错误 (404):
{ "status": "error", "message": "Entity not found" }
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${entityId}`, {
headers: { Authorization: `Bearer ${api_key}` }
});
更新实体
PUT /spaces/:spaceId/entities/:entityId
使用合并语义更新实体:提供的字段覆盖现有值,未设置的字段保持不变。实体的 state 也会被保留。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
Content-Type | application/json | 是 | JSON 请求体 |
请求体参数:
| 字段 | 类型 | 必需 | 描述 |
|---|---|---|---|
entity_data | object | 是 | 要合并到现有实体的字段 |
响应:
{
"status": "ok",
"data": {
"entity_id": "user-model-91de72809185c9fa",
"entity_type": "UserModelEntity",
"entity_data": {
"glbUrl": "https://s3.amazonaws.com/...",
"scale": [3, 3, 3],
"rotation": { "x": 0, "y": 0, "z": 0 },
"label": "My Label"
}
}
}
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${entityId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
entity_data: {
scale: [3, 3, 3],
label: "My Label"
}
})
});
删除实体
DELETE /spaces/:spaceId/entities/:entityId
永久删除空间中的实体。
请求
标头:
| 标头 | 值 | 必需 | 描述 |
|---|---|---|---|
Authorization | Bearer <api_key> | 是 | 用户的 API 密钥 |
路径参数:
| 参数 | 类型 | 必需 | 描述 |
|---|---|---|---|
spaceId | string | 是 | 空间 ID(来自空间 URL) |
entityId | string | 是 | 实体 ID |
响应:
{
"status": "ok",
"data": { "message": "Entity deleted successfully" }
}
请求示例
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${entityId}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${api_key}` }
});
实体管理示例
async function manageEntities(apiKey, spaceId, resourceKey) {
const headers = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
};
const base = `${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities`;
// Create entity from uploaded file
const createRes = await fetch(base, {
method: "POST",
headers,
body: JSON.stringify({
resource_key: resourceKey,
entity_data: { scale: [2, 2, 2] }
})
});
const { data: created } = await createRes.json();
console.log("Created:", created.entity_id);
// List all entities
const listRes = await fetch(base, { headers });
const { data: list } = await listRes.json();
console.log("Entities in space:", list.count);
// Update entity
await fetch(`${base}/${created.entity_id}`, {
method: "PUT",
headers,
body: JSON.stringify({ entity_data: { label: "Updated" } })
});
// Delete entity
await fetch(`${base}/${created.entity_id}`, {
method: "DELETE",
headers
});
}
传送门
传送门是连接空间的传送入口。有两种类型:
-
静态传送门 — 围绕 hub 房间排列的 7 个固定传送门框架。创建 hub 空间时自动创建(在
POST /user/create-space中使用space_type: "hub")。您可以为它们填充内容或留空,但不能添加或删除。静态传送门在 infinite 空间中不存在 — 因为 infinite 空间没有房间架构,所以没有传送门框架可以显示。 -
动态传送门 — 自由定位的传送门,可放置在 3D 空间的任何位置。您可以在 hub 和 infinite 空间中创建任意数量。
两种传送门类型都通过上述实体端点进行管理。
静态传送门仅在 hub 空间中有意义(其中 hideArchitecture 为 false)。当您使用 space_type: "infinite"(默认值)创建空间时,不会创建静态传送门,因为房间架构被隐藏了。如果您需要在 infinite 空间中使用传送门,请使用动态传送门。
静态传送门(类型:Gate)
静态传送门在 hub 空间中自动创建。它们的 3D 位置由客户端固定 — 您只能控制其内容(标题、链接、描述、缩略图等)。
每个 hub 空间都有相同的 7 个传送门实体 ID。使用这些固定 UUID 通过 PUT /spaces/:spaceId/entities/:entityId 直接操作传送门:
| 传送门 | 实体 ID |
|---|---|
| 0 | 9769e4b4-e5d9-4286-9353-c2c66b158347 |
| 1 | 1aef8fd4-6447-4e04-969a-4669c11dd52e |
| 2 | 75f16b87-5314-4cdf-a8d5-bb2a8292b687 |
| 3 | 608f2483-802b-4fa7-95bb-920bed1431ad |
| 4 | fcc35518-309c-47e1-8d93-d939d6bca475 |
| 5 | 404c7fe3-26f1-4fb6-bac7-a50bf1f3cb1a |
| 6 | b9a67fa0-00ce-401d-94c2-4045c4aaec35 |
示例 — 将传送门 3 设为链接到另一个空间:
const GATE_IDS = [
"9769e4b4-e5d9-4286-9353-c2c66b158347", // Gate 0
"1aef8fd4-6447-4e04-969a-4669c11dd52e", // Gate 1
"75f16b87-5314-4cdf-a8d5-bb2a8292b687", // Gate 2
"608f2483-802b-4fa7-95bb-920bed1431ad", // Gate 3
"fcc35518-309c-47e1-8d93-d939d6bca475", // Gate 4
"404c7fe3-26f1-4fb6-bac7-a50bf1f3cb1a", // Gate 5
"b9a67fa0-00ce-401d-94c2-4045c4aaec35", // Gate 6
];
// Internal link — point gate 3 to another space
await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${GATE_IDS[3]}`, {
method: "PUT",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({
entity_data: {
title: "My Portal",
link: "42485456_5076",
platform: "arrival.space",
description: "Step through to see my gallery"
}
})
});
// External link — point gate 0 to a web page
// Set videoURL to the same URL so the page renders on the gate surface
await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${GATE_IDS[0]}`, {
method: "PUT",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({
entity_data: {
title: "Visit Our Website",
link: "https://example.com",
platform: "<auto-detect>",
videoURL: "https://example.com"
}
})
});
传送门内容字段(entity_data):
| 字段 | 类型 | 描述 |
|---|---|---|
title | string | 显示在传送门框架上的标题 |
link | string | 内部链接的空间 ID(例如 "42485456_5076")或外部链接的完整 URL(例如 "https://example.com")。留空表示无链接 |
platform | string | 内部空间链接使用 "arrival.space",外部 URL 使用 "<auto-detect>"。必须与 link 类型匹配 |
description | string | 描述文本(支持 HTML) |
videoURL | string | 显示在传送门表面的图片 URL 或网页 URL。对于外部链接,将其设置为与 link 相同的 URL 以在传送门上渲染页面 |
logoURL | string | Logo 图片 URL(传送门上的小图标) |
logoLinkURL | string | 点击 logo 时打开的 URL |
content360Enabled | boolean | 启用 360° 内容模式 |
contentSynchronized | boolean | 在用户间同步内容 |
openAsTab | boolean | 在新浏览器标签页中打开链接,而非导航跳转 |
enableWebStream | boolean | 启用网页流式传输 |
enableWebProxy | boolean | 启用网页代理 |
embeddedEnabled | boolean | 启用嵌入内容 |
screenSelect | number | 屏幕选择索引(默认:0) |
利用合并语义的优势 — 您只需发送要更改的字段。布尔标志和其他字段将保持默认值。
动态传送门(类型:DynamicGate)
动态传送门与静态传送门具有相同的内容字段,另外还有一个 dynamicGate 对象,定义它们的 3D 位置、旋转和缩放。您可以创建任意数量。
使用 POST /spaces/:spaceId/entities 创建动态传送门。
附加字段:
| 字段 | 类型 | 描述 |
|---|---|---|
dynamicGate | object | 动态传送门必需。包含 3D 变换数据 |
dynamicGate.position | {x, y, z} | 世界位置 |
dynamicGate.rotation | {x, y, z} | 欧拉旋转角度(度) |
dynamicGate.scale | {w, h} | 宽度和高度缩放(默认:{w: 1, h: 1}) |
dynamicGate.frameless | boolean | 隐藏传送门框架(false = 显示框架) |
dynamicGate.hidden | boolean | 完全隐藏传送门 |
示例 — 创建动态传送门:
await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
entity_id: "my-dynamic-gate",
entity_type: "DynamicGate",
entity_data: {
dynamicGate: {
position: { x: 5.0, y: 0.0, z: -3.0 },
rotation: { x: 0, y: 45, z: 0 },
frameless: false,
hidden: false,
scale: { w: 1, h: 1 }
},
title: "Side Gallery",
link: "42485456_5076",
platform: "arrival.space",
description: "A freely-placed portal"
}
})
});
清除传送门
要清除静态传送门(移除内容但保留槽位),使用空值更新它:
await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${gateId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
entity_data: {
title: "",
link: "",
description: "",
videoURL: "",
logoURL: "",
logoLinkURL: ""
}
})
});
要完全移除动态传送门,使用 DELETE /spaces/:spaceId/entities/:entityId。
不要删除静态传送门 — 它们是房间结构的一部分。只需清除它们的内容。删除静态传送门将留下无法通过 API 恢复的空槽位。
错误处理
| 状态码 | 描述 |
|---|---|
200 | OK — 请求成功 |
201 | Created — 资源创建成功(例如实体) |
202 | Accepted — 异步任务已启动(轮询完成状态) |
204 | No Content — 异步任务更新标头 |
400 | Bad Request — 参数无效 |
401 | Unauthorized — API 密钥无效或已过期 |
403 | Forbidden — 权限不足 |
404 | Not Found — 资源或任务未找到 |
413 | Payload Too Large — 文件超过大小限制 |
500 | Internal Server Error — 服务器内部错误 |
507 | Insufficient Storage — 用户存储配额已超出 |
注意事项
- API 密钥与单个用户账户绑定
- 上传的文件创建的空间归 API 密钥持有者所有
- 预签名 URL 会在有限时间后过期 — 请及时上传
- 妥善保管您的 API 密钥,切勿在客户端代码中暴露