REST API
Überblick
Die Arrival Space REST API ermöglicht es Drittanbieter-Anwendungen, Benutzer zu authentifizieren, Assets über vorsignierte URLs hochzuladen, asynchrone Verarbeitungs-Jobs zu verfolgen, Spaces zu erstellen und zu verwalten, Entities zu verwalten und Gates zu konfigurieren.
Funktionen
- Assets mit vorsignierten S3-URLs hochladen und Verarbeitung bestätigen
- Asynchrone Verarbeitungs-Jobs mit Echtzeit-Fortschritt verfolgen
- Spaces aus Uploads erstellen und Datenschutzeinstellungen aktualisieren
- Entities innerhalb von Spaces erstellen, lesen, aktualisieren und löschen
- Statische und dynamische Gates über Entity-Endpunkte konfigurieren
Stellen Sie sicher, dass Sie genügend verfügbaren Speicherplatz haben, bevor Sie den Upload starten. Andernfalls kann der Upload fehlschlagen.
Basis-URL & Versionierung
const API_BASE_URL = "https://api-staging.arrival.space/api";
const VERSION = "v1";
Alle Endpunkte sollten mit ${API_BASE_URL}/${VERSION} vorangestellt werden.
Der API-Endpunkt kann sich in Zukunft ändern. Wenn Sie Verbindungsprobleme haben, wenden Sie sich bitte an unser Support-Team.
Authentifizierung
Alle API-Anfragen erfordern einen Bearer-Token im Authorization-Header:
Authorization: Bearer <api_key>
Option 1: Benutzer-Account API-Schlüssel
- Melden Sie sich bei Arrival Space an
- Navigieren Sie zu Ihren Kontoeinstellungen
- Generieren oder kopieren Sie Ihren API-Schlüssel
Option 2: OAuth 2.0 Login (für native Apps)
Native Anwendungen können OAuth 2.0 Authorization Code Flow mit PKCE verwenden, damit sich Benutzer über ihr Arrival.Space-Konto anmelden können (Google-Anmeldung oder E-Mail/Passwort). Dies ist der empfohlene Ansatz für Drittanbieter-Integrationen, bei denen Benutzer API-Schlüssel nicht manuell kopieren müssen.
Der OAuth-Flow verwendet Standard-Endpunkte und gibt einen API-Schlüssel als Access-Token zurück — denselben Schlüssel, der in den Kontoeinstellungen des Benutzers sichtbar ist. Das bedeutet, dass der Token Server-Neustarts übersteht und identisch über alle API-Endpunkte funktioniert.
OAuth-Endpunkte:
| Endpunkt | Beschreibung |
|---|---|
GET /.well-known/oauth-authorization-server | OAuth 2.0 Server-Metadaten (Auto-Discovery) |
POST /register | Dynamische Client-Registrierung (RFC 7591) |
GET /authorize | Autorisierungs-Endpunkt — leitet zur Login-Seite weiter |
POST /token | Token-Austausch — gibt den Access-Token zurück |
POST /revoke | Token-Widerruf |
Ablauf-Übersicht:
-
Client registrieren (einmalig oder beim ersten Start):
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-Challenge generieren und Benutzer zur Autorisierung weiterleiten:
// 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));Der Benutzer sieht die Arrival.Space-Anmeldeseite mit Google-Anmeldung und E-Mail/Passwort-Optionen.
-
Callback verarbeiten — nach der Anmeldung leitet der Browser zu Ihrer
redirect_urimit?code=...&state=...weiter:// 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; -
Token verwenden — es ist ein Standard-API-Schlüssel, verwenden Sie ihn wie jeden anderen:
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces`, {
headers: { Authorization: `Bearer ${apiKey}` }
});
- Der vom OAuth-Flow zurückgegebene
access_tokenist der tatsächliche API-Schlüssel des Benutzers — derselbe, der in seinen Kontoeinstellungen angezeigt wird - Token laufen nicht ab (kein
expires_in-Limit) — sie bestehen, bis der Benutzer den Schlüssel widerruft - Es wird kein Refresh-Token benötigt, da der Access-Token nicht abläuft
- Wenn der Benutzer bereits einen API-Schlüssel hat, wird der vorhandene Schlüssel zurückgegeben (keine Duplikate erstellt)
Wenn Ihr Server hinter einem Reverse-Proxy (Apache, nginx) sitzt, stellen Sie sicher, dass /.well-known/oauth-authorization-server an das Node.js-Backend weitergeleitet wird. Wenn die Discovery nicht verfügbar ist, können Clients die Endpunkte direkt verwenden: /authorize, /token, /register.
Die OAuth-Endpunkte sind auch kompatibel mit dem Model Context Protocol (MCP). MCP-Clients wie Claude Desktop können sich direkt über https://api-dev.arrival.space/api/v1/mcp verbinden — der OAuth-Flow wird automatisch vom MCP-Client übernommen.
Konventionen
- Antworten verwenden einen gemeinsamen Umschlag mit
status,dataund manchmalmessageodererror - Zeitstempel sind ISO 8601 Zeichenketten (z.B.
2025-01-15T10:30:00.000Z) - Einige Operationen sind asynchron und geben eine
job_idzurück, die Sie überGET /jobs/:jobIdabfragen, bisjob_statuscompletedoderfailedist resource_keyidentifiziert ein hochgeladenes Asset und wird verwendet, um Spaces oder Entities zu erstellenspaceIdist der eindeutige Bezeichner in der Space-URL (z.B.https://arrival.space/12345678_9012)entity_ididentifiziert eine bestimmte Entity innerhalb eines Space
Ratenbegrenzungen
Limits werden pro API-Schlüssel durchgesetzt (Fallback auf IP, wenn kein Schlüssel angegeben). Eine 429-Antwort enthält einen Retry-After-Header und ein retry_after_seconds-Feld im Body.
| Bereich | Limit |
|---|---|
| Alle API-Endpunkte | 300 Anfragen/Min |
POST /files/upload | 20 Anfragen/Min |
POST /files/upload-complete | 20 Anfragen/Min |
GET /jobs/:jobId | 120 Anfragen/Min |
POST /user/create-space | 60 Anfragen/Min |
POST /spaces/update-privacy | 60 Anfragen/Min |
Entity-CRUD (/spaces/:spaceId/entities*) | 300 Anfragen/Min |
Schnellstart
Ablauf-Zusammenfassung
- API-Schlüssel beschaffen (Benutzer-Account-Schlüssel oder OAuth-Login)
- Vorsignierte Upload-URL anfordern
- Datei auf S3 hochladen
- Upload bestätigen und bei Bedarf auf asynchrone Verarbeitungsfertigstellung abfragen
- Space erstellen und Entities sowie Gates verwalten
Vollständiges Integrationsbeispiel
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-Referenz
Uploads
Upload-URL anfordern
POST /files/upload
Generiert eine vorsignierte S3-URL für den Datei-Upload.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Content-Type | application/json | Ja | JSON-Anfragebody |
Body-Parameter:
| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
file_name | string | Ja | Name der Datei |
file_size | number | Ja | Dateigröße in Bytes |
content_type | string | Ja | MIME-Typ |
Antwort
{
"status": "ok",
"data": {
"upload_type": "presigned_url",
"params": {
"url": "https://s3.amazonaws.com/bucket/...?X-Amz-Signature=...",
"method": "PUT",
"headers": { "Content-Type": "application/zip" }
}
}
}
Beispielanfrage
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"
})
});
Auf S3 hochladen
Sobald Sie die vorsignierte URL erhalten haben, verwenden Sie die zurückgegebenen Parameter, um die Datei direkt auf S3 hochzuladen:
fetch(params.url, {
method: params.method,
headers: params.headers,
body: file,
});
Upload bestätigen
POST /files/upload-complete
Rufen Sie dies auf, nachdem die Datei erfolgreich auf S3 hochgeladen wurde. Bestätigt den Abschluss des Uploads und gibt den Resource-Key zurück, den Sie später verwenden können, um einen Space oder andere Entities zu erstellen.
Für .zip-Dateien oder Dateien über 1,5 GB gibt dieser Endpunkt eine Job-ID anstelle des Resource-Key direkt zurück. Sie müssen den Job-Status-Endpunkt abfragen, um den endgültigen resource_key zu erhalten, sobald die Verarbeitung abgeschlossen ist. Siehe Job-Status abrufen für Details.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Content-Type | application/json | Ja | JSON-Anfragebody |
Body-Parameter:
| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
status | string | Ja | Verwenden Sie "success" nach Abschluss des Uploads |
extra_info.file_url | string | Ja | URL der hochgeladenen Datei in S3 |
Antwort (Kleine Dateien)
Für Dateien unter 1,5 GB (kein Zip) wird der Resource-Key sofort zurückgegeben:
{
"status": "confirmed",
"data": {
"resource_key": "api-uploads/...ply"
}
}
Antwort (Große Dateien - Asynchrone Verarbeitung)
Für .zip-Dateien oder Dateien über 1,5 GB wird eine Job-ID zum Abfragen zurückgegeben:
{
"status": "processing",
"message": "Large file queued for processing",
"data": {
"job_id": "a1b2c3d4e5f6...",
"poll_url": "/api/v1/jobs/a1b2c3d4e5f6..."
}
}
| Feld | Beschreibung |
|---|---|
status | "processing" zeigt an, dass ein asynchroner Job gestartet wurde |
job_id | Eindeutiger Bezeichner zum Abfragen des Job-Status |
poll_url | Komfort-URL zum Abfragen |
Beispielanfrage
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"
}
})
});
Jobs
Job-Status abrufen
GET /jobs/:jobId
Fragen Sie diesen Endpunkt ab, um den Status eines asynchronen Jobs zu überprüfen (z.B. Zip-Datei-Verarbeitung oder Kopieren großer Dateien). Der Endpunkt liefert Echtzeit-Fortschrittsaktualisierungen. Fragen Sie weiter ab, bis job_status "completed" oder "failed" ist.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
URL-Parameter:
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
jobId | string | Ja | Die von upload-complete zurückgegebene Job-ID |
Antwort
{
"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"
}
}
Beispiel für abgeschlossenen Job:
{
"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"
}
}
Job-Status-Werte
| Status | Beschreibung |
|---|---|
queued | Job wartet auf Verarbeitung |
processing | Job wird aktuell verarbeitet |
completed | Job erfolgreich abgeschlossen — result.resource_key ist verfügbar |
failed | Job fehlgeschlagen — Details im error-Feld |
Antwort-Felder
| Feld | Typ | Beschreibung |
|---|---|---|
job_id | string | Eindeutiger Job-Bezeichner |
job_status | string | Aktueller Status des Jobs |
progress | number | Fortschritt in Prozent (0-100), wird während der Verarbeitung in Echtzeit aktualisiert |
message | string | Menschenlesbare Statusmeldung mit Fortschrittsdetails (z.B. "Copying file... 45% (2.3 GB / 5.0 GB)") |
result | object | Ergebnisdaten bei Abschluss (enthält resource_key) |
error | string | Fehlermeldung bei fehlgeschlagenem Job |
created_at | string | ISO-Zeitstempel der Job-Erstellung |
updated_at | string | ISO-Zeitstempel der letzten Statusaktualisierung |
Beispielanfrage
const response = await fetch(`${API_BASE_URL}/${VERSION}/jobs/${jobId}`, {
headers: { Authorization: `Bearer ${api_key}` }
});
Sowohl für Zip-Dateien als auch für große Dateien (über 1,5 GB) werden die Felder progress und message in Echtzeit aktualisiert, während die Datei verarbeitet wird. Sie können diesen Endpunkt alle 1-2 Sekunden abfragen, um Live-Fortschrittsaktualisierungen zu erhalten, die Folgendes anzeigen:
- Prozentsatz des Fortschritts
- Menge der verarbeiteten Daten (MB oder GB)
- Aktuelle Operation (z.B. "Copying file...", "Extracting zip...", "Uploading files...")
Beispiel: Abfrage bis zum Job-Abschluss
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();
});
}
Jobs auflisten
GET /jobs
Listet alle Jobs des authentifizierten Benutzers auf. Nützlich zum Verfolgen mehrerer asynchroner Operationen.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Antwort
{
"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
}
}
Beispielanfrage
const response = await fetch(`${API_BASE_URL}/${VERSION}/jobs`, {
headers: { Authorization: `Bearer ${api_key}` }
});
Jobs werden nach 1 Stunde automatisch bereinigt.
Spaces
Space erstellen
POST /user/create-space
Erstellt einen neuen Space in Arrival Space mit dem Schlüssel zur hochgeladenen Datei. Rufen Sie dies nach der Bestätigung des Uploads auf.
Space-Typen
Es gibt zwei Arten von Spaces:
| Typ | Beschreibung |
|---|---|
"infinite" | Standard. Offener Space ohne Raumarchitektur. Der 3D-Inhalt schwebt in einer offenen Umgebung. Es werden keine statischen Gate-Rahmen erstellt. Am besten geeignet für die Präsentation einzelner 3D-Modelle. |
"hub" | Raum mit Architektur — Wände, Boden, Decke und 7 statische Gate-Rahmen, die um den Raum angeordnet sind. Am besten geeignet für die Erstellung eines Home-Space mit Portalen zu anderen Spaces. |
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Content-Type | application/json | Ja | JSON-Anfragebody |
Body-Parameter:
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
title | string | Nein | Space-Titel (wird automatisch aus dem Dateinamen erkannt, wenn weggelassen) |
description | string | Nein | Space-Beschreibung |
resource_key | string | Nein | Resource-Key von upload-complete. Wenn weggelassen, wird ein leerer Space erstellt |
space_type | string | Nein | "hub" oder "infinite". Standard: "infinite" |
Antwort
{
"status": "ok",
"message": "Space created successfully",
"data": {
"space_url": "https://arrival.space/12345678_9012",
"title": "My New Space",
"space_type": "hub"
}
}
Beispielanfrage
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"
}
})
});
Space-Datenschutz aktualisieren
POST /spaces/update-privacy
Aktualisiert die Datenschutzeinstellung eines bestehenden Space. Erfordert Besitzerrechte am Space.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Content-Type | application/json | Ja | JSON-Anfragebody |
Body-Parameter:
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
spaceId | string | Ja | Der eindeutige Bezeichner des Space (in der Space-URL zu finden) |
roomPrivacy | string | Ja | Datenschutzeinstellung. Eine von: "Public", "Private", "Link Only" |
informFollowers | boolean | Nein | Wenn true, werden Follower benachrichtigt, wenn der Space auf "Public" gesetzt wird |
Datenschutzoptionen
| Option | Beschreibung |
|---|---|
Public | Space ist öffentlich sichtbar und auffindbar. Follower können benachrichtigt werden. |
Private | Space ist privat und nicht auffindbar. Nur über Direktlink durch den Besitzer zugänglich. |
Link Only | Space ist über Direktlink zugänglich, aber nicht öffentlich gelistet. Erfordert Pro-Abonnement. |
Antwort
Erfolg:
{
"status": "ok",
"message": "Space privacy updated successfully"
}
Fehlerantworten:
// 404 - Space nicht gefunden
{
"status": "error",
"message": "Space not found"
}
// 403 - Nicht autorisiert
{
"status": "error",
"message": "You are not authorized to update the privacy of this space"
}
// 400 - Ungültige Datenschutzoption
{
"status": "error",
"message": "Invalid privacy option"
}
// 400 - Pro erforderlich für Link Only
{
"status": "error",
"message": "You need to be a Pro user to set a space to Link Only"
}
Beispielanfrage
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));
Entities
Entities sind Objekte innerhalb eines Space (3D-Modelle, Portale, Medien usw.). Diese Endpunkte ermöglichen es Ihnen, Entities in Spaces, die Sie besitzen, zu erstellen, zu lesen, zu aktualisieren und zu löschen.
Alle Entity-Endpunkte erfordern den Authorization: Bearer <api_key>-Header und Space-Besitzerrechte.
Entity erstellen
POST /spaces/:spaceId/entities
Erstellt eine neue Entity in einem Space. Wenn resource_key angegeben wird, wird die Entity aus einer hochgeladenen Datei mit automatisch erkannten Standardwerten (Skalierung, Rotation basierend auf Dateityp) erstellt. Wenn weggelassen, wird eine leere Entity erstellt.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Content-Type | application/json | Ja | JSON-Anfragebody |
Body-Parameter:
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
resource_key | string | Nein | S3-Schlüssel von upload-complete. Löst dateibasierte Entity-Erstellung mit automatisch erkannter Skalierung/Rotation aus |
entity_id | string | Nein | Benutzerdefinierte Entity-ID. Wird automatisch generiert, wenn weggelassen |
entity_type | string | Nein | Entity-Typ (z.B. "UserModelEntity", "Simple", "Gate") |
entity_data | object | Nein | Entity-Daten. Werden mit automatisch erkannten Standardwerten zusammengeführt, wenn resource_key angegeben ist |
Antwort (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]
}
}
}
Beispielanfrage
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]
}
})
});
Entities auflisten
GET /spaces/:spaceId/entities
Gibt alle Entities in einem Space zurück (ausgenommen die interne RoomInfo-Entity).
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Query-Parameter:
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
limit | number | Nein | Maximale Anzahl zurückzugebender Entities (Standard 50, Maximum 200) |
cursor | string | Nein | Paginierungs-Cursor von nextCursor |
Antwort:
{
"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
}
}
Beispielanfrage
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities?limit=50`, {
headers: { Authorization: `Bearer ${api_key}` }
});
Entity abrufen
GET /spaces/:spaceId/entities/:entityId
Gibt eine einzelne Entity anhand der ID zurück.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Pfad-Parameter:
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
spaceId | string | Ja | Space-ID (aus der Space-URL) |
entityId | string | Ja | Entity-ID |
Antwort:
{
"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"
}
}
Fehler (404):
{ "status": "error", "message": "Entity not found" }
Beispielanfrage
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${entityId}`, {
headers: { Authorization: `Bearer ${api_key}` }
});
Entity aktualisieren
PUT /spaces/:spaceId/entities/:entityId
Aktualisiert eine Entity mit Merge-Semantik: Angegebene Felder überschreiben vorhandene Werte, nicht gesetzte Felder werden beibehalten. Der state der Entity wird ebenfalls beibehalten.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Content-Type | application/json | Ja | JSON-Anfragebody |
Body-Parameter:
| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
entity_data | object | Ja | Felder, die mit der bestehenden Entity zusammengeführt werden |
Antwort:
{
"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"
}
}
}
Beispielanfrage
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"
}
})
});
Entity löschen
DELETE /spaces/:spaceId/entities/:entityId
Löscht eine Entity dauerhaft aus einem Space.
Anfrage
Headers:
| Header | Wert | Erforderlich | Beschreibung |
|---|---|---|---|
Authorization | Bearer <api_key> | Ja | API-Schlüssel des Benutzers |
Pfad-Parameter:
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
spaceId | string | Ja | Space-ID (aus der Space-URL) |
entityId | string | Ja | Entity-ID |
Antwort:
{
"status": "ok",
"data": { "message": "Entity deleted successfully" }
}
Beispielanfrage
const response = await fetch(`${API_BASE_URL}/${VERSION}/spaces/${spaceId}/entities/${entityId}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${api_key}` }
});
Beispiel zur Entity-Verwaltung
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
});
}
Gates
Gates sind Portale, die Spaces miteinander verbinden. Es gibt zwei Arten:
-
Statische Gates — 7 feste Portal-Rahmen, die um einen Hub-Raum angeordnet sind. Sie werden automatisch erstellt, wenn ein Hub-Space angelegt wird (
space_type: "hub"inPOST /user/create-space). Sie können sie mit Inhalt füllen oder leer lassen, aber Sie können sie nicht hinzufügen oder entfernen. Statische Gates existieren nicht in Infinite-Spaces — da Infinite-Spaces keine Raumarchitektur haben, gibt es keine Gate-Rahmen zur Anzeige. -
Dynamische Gates — frei positionierbare Portale, die überall im 3D-Raum platziert werden können. Sie können beliebig viele davon in beiden Hub- und Infinite-Spaces erstellen.
Beide Gate-Typen werden über die oben beschriebenen Entity-Endpunkte verwaltet.
Statische Gates sind nur in Hub-Spaces sinnvoll (wo hideArchitecture auf false steht). Wenn Sie einen Space mit space_type: "infinite" (Standard) erstellen, werden keine statischen Gates erzeugt, da die Raumarchitektur ausgeblendet ist. Verwenden Sie dynamische Gates, wenn Sie Portale in einem Infinite-Space benötigen.
Statische Gates (Typ: Gate)
Statische Gates werden automatisch in Hub-Spaces erstellt. Ihre 3D-Positionen werden vom Client festgelegt — Sie steuern nur deren Inhalt (Titel, Link, Beschreibung, Vorschaubild usw.).
Jeder Hub-Space hat die gleichen 7 Gate-Entity-IDs. Verwenden Sie diese festen UUIDs, um Gates direkt über PUT /spaces/:spaceId/entities/:entityId anzusprechen:
| Gate | Entity-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 |
Beispiel — Gate 3 auf einen anderen Space verlinken:
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"
}
})
});
Gate-Inhaltsfelder (entity_data):
| Feld | Typ | Beschreibung |
|---|---|---|
title | string | Gate-Titel, der auf dem Portal-Rahmen angezeigt wird |
link | string | Space-ID für interne Links (z.B. "42485456_5076") oder eine vollständige URL für externe Links (z.B. "https://example.com"). Leer lassen für keinen Link |
platform | string | "arrival.space" für interne Space-Links, "<auto-detect>" für externe URLs. Muss zum link-Typ passen |
description | string | Beschreibungstext (unterstützt HTML) |
videoURL | string | Bild-URL oder Webseiten-URL, die auf der Gate-Oberfläche angezeigt wird. Für externe Links setzen Sie dies auf dieselbe URL wie link, um die Seite auf dem Gate darzustellen |
logoURL | string | Logo-Bild-URL (kleines Symbol auf dem Gate) |
logoLinkURL | string | URL, die geöffnet wird, wenn das Logo angeklickt wird |
content360Enabled | boolean | 360-Grad-Inhaltsmodus aktivieren |
contentSynchronized | boolean | Inhalt benutzerübergreifend synchronisieren |
openAsTab | boolean | Link in einem neuen Browser-Tab öffnen statt zu navigieren |
enableWebStream | boolean | Web-Streaming aktivieren |
enableWebProxy | boolean | Web-Proxy aktivieren |
embeddedEnabled | boolean | Eingebetteten Inhalt aktivieren |
screenSelect | number | Bildschirmauswahl-Index (Standard: 0) |
Nutzen Sie die Merge-Semantik zu Ihrem Vorteil — Sie müssen nur die Felder senden, die Sie ändern möchten. Die booleschen Flags und anderen Felder behalten ihre Standardwerte.
Dynamische Gates (Typ: DynamicGate)
Dynamische Gates haben die gleichen Inhaltsfelder wie statische Gates, plus ein dynamicGate-Objekt, das ihre 3D-Position, Rotation und Skalierung definiert. Sie können beliebig viele davon erstellen.
Erstellen Sie dynamische Gates mit POST /spaces/:spaceId/entities.
Zusätzliches Feld:
| Feld | Typ | Beschreibung |
|---|---|---|
dynamicGate | object | Erforderlich für dynamische Gates. Enthält 3D-Transformationsdaten |
dynamicGate.position | {x, y, z} | Weltposition |
dynamicGate.rotation | {x, y, z} | Euler-Rotation in Grad |
dynamicGate.scale | {w, h} | Breiten- und Höhenskalierung (Standard: {w: 1, h: 1}) |
dynamicGate.frameless | boolean | Portal-Rahmen ausblenden (false = Rahmen anzeigen) |
dynamicGate.hidden | boolean | Gate vollständig ausblenden |
Beispiel — Ein dynamisches Gate erstellen:
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"
}
})
});
Ein Gate leeren
Um ein statisches Gate zu leeren (seinen Inhalt entfernen, aber den Slot behalten), aktualisieren Sie es mit leeren Werten:
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: ""
}
})
});
Um ein dynamisches Gate vollständig zu entfernen, verwenden Sie DELETE /spaces/:spaceId/entities/:entityId.
Löschen Sie keine statischen Gates — sie sind Teil der Raumstruktur. Leeren Sie nur deren Inhalt. Das Löschen eines statischen Gates hinterlässt einen leeren Slot, der über die API nicht wiederhergestellt werden kann.
Fehlerbehandlung
| Statuscode | Beschreibung |
|---|---|
200 | OK — Anfrage erfolgreich |
201 | Erstellt — Ressource erfolgreich erstellt (z.B. Entity) |
202 | Akzeptiert — Asynchroner Job gestartet (Abfrage bis zum Abschluss) |
204 | Kein Inhalt — Asynchrone Job-Header aktualisiert |
400 | Fehlerhafte Anfrage — Ungültige Parameter |
401 | Nicht autorisiert — Ungültiger oder abgelaufener API-Schlüssel |
403 | Verboten — Unzureichende Berechtigungen |
404 | Nicht gefunden — Ressource oder Job nicht gefunden |
413 | Nutzlast zu groß — Datei überschreitet Größenlimit |
500 | Interner Serverfehler |
507 | Unzureichender Speicher — Benutzerspeicherkontingent überschritten |
Hinweise
- API-Schlüssel sind an individuelle Benutzerkonten gebunden
- Hochgeladene Dateien erstellen Spaces, die dem Inhaber des API-Schlüssels gehören
- Vorsignierte URLs laufen nach begrenzter Zeit ab — laden Sie zeitnah hoch
- Bewahren Sie Ihren API-Schlüssel sicher auf und stellen Sie ihn niemals in clientseitigem Code bereit