Splats per Schnittstelle bestellen
Ein fremdes System beauftragt eine Aufnahme, verfolgt sie und importiert das fertige Paket — ohne dass ein Mensch durch unsere Oberfläche geht. Diese Seite beschreibt den Vertrag: Ablauf, Zustände, Webhooks und das, was am Ende herauskommt.
Gedacht ist sie für Plattformen, Digital-Twin-Software und Viewer, die Räume ihrer eigenen Kundschaft zeigen. Die Schnittstelle ist ein Nebenweg, nicht der Hauptweg: Wer selbst mit dem Handy aufnimmt, braucht sie nicht.
Der Ablauf in vier Schritten
Anlegen, hochladen, abschließen, verfolgen. Jeder Aufruf braucht einen Bearer-Token deines Zugangs; die Beispiele schreiben ihn als sk_…
Anlegen
POST /api/v1/captures mit Dateiname und Größe, optional Standort, Name, eigener Referenz (clientRef), Qualität und Ablauf. Die Antwort enthält presignte Teile für den Upload.
Direkt in den Bucket hochladen
Jeder Teil geht per PUT an seine presignte URL — an uns vorbei. Die Steuerungsebene sieht die Bytes nie; sie erfährt nur, dass sie angekommen sind.
Abschließen
POST /api/v1/captures/:id/complete mit den ETags der Teile. Ab hier läuft es von selbst: Vorab-Check, Start, Prüfschritte, Training, Kollisionsmesh, Paket.
Verfolgen und abholen
GET /api/v1/captures/:id pollen, bis der Zustand done ist — oder einen Webhook registrieren. Das Paket liefert GET /api/v1/captures/:id/result.
curl -X POST https://splatastic.com/api/v1/captures \
-H "Authorization: Bearer sk_…" -H "Content-Type: application/json" \
-d '{
"filename": "rundgang.mp4",
"sizeBytes": 214748364,
"siteName": "Halle 12",
"clientRef": "acme:asset-42",
"quality": "standard",
"flow": "headless"
}'{
"id": "cap_…",
"siteId": "site_…",
"clientRef": "acme:asset-42",
"state": "uploading",
"upload": {
"uploadId": "up_…",
"key": "captures/cap_…/sources/….mp4",
"partSize": 16777216,
"partCount": 13,
"parts": [{ "partNumber": 1, "url": "https://…" }, "…"]
}
}curl -X POST https://splatastic.com/api/v1/captures/cap_…/complete \
-H "Authorization: Bearer sk_…" -H "Content-Type: application/json" \
-d '{
"uploadId": "up_…",
"parts": [{ "partNumber": 1, "etag": "\"…\"" }, "…"]
}'{
"id": "cap_…", "name": "Halle 12", "siteId": "site_…",
"clientRef": "acme:asset-42", "flow": "headless",
"state": "processing",
"stage": "training",
"progress": 0.62,
"etaSeconds": 480,
"needsReview": null,
"error": null,
"hasResult": false,
"createdAt": 1757500000000,
"updatedAt": 1757500600000
}Der Zustandsautomat: sieben Werte
state ist der Vertrag. stage daneben nennt die interne Stufe und ist rein informativ — verlass dich nicht darauf.
| state | Bedeutung |
|---|---|
uploading | Noch keine Quelle vollständig registriert — der Upload läuft oder hat nicht begonnen. |
checking | Das Material ist da, der Vorab-Check läuft oder der automatische Start steht unmittelbar bevor. |
queued | Ein Schritt wartet auf einen Platz in der Warteschlange oder auf einen startenden GPU-Pod. |
processing | Ein Schritt rechnet tatsächlich. |
needs-review | Ein Mensch wird gebraucht: ein Prüfschritt wartet, der automatische Start konnte nicht, oder der laufende Schritt wurde angehalten. needsReview nennt Grund, Text und einen Deep-Link. |
done | Ausgeliefert — GET …/result gibt die Dateien heraus. |
failed | Ein Schritt ist fehlgeschlagen; error trägt die Meldung. |
Abläufe: wie viel Mensch der Auftrag braucht
flow beim Anlegen entscheidet, ob ein gelbes oder rotes Prüfurteil anhält oder durchläuft.
| flow | Bedeutung |
|---|---|
headless | Vorgabe. Startet von selbst, sobald der Vorab-Check nicht rot ist; eine grüne Ampel läuft durch, gelb oder rot hält an und wird needs-review. |
unattended | Für ein System, das das Ergebnis ohnehin selbst prüft: jede Ampel läuft durch, angehalten wird nur bei einem Fehler. Das Urteil steht trotzdem in den Messwerten der Aufnahme. |
auto | Wie headless, hält aber zusätzlich am Splat-Gate an. |
guided | Startet erst auf Klick und hält an jedem Prüfschritt — der Weg für einen Menschen an der Oberfläche. |
unattended ist für ein System gedacht, das das Ergebnis ohnehin selbst prüft und nur Splat und Kollisionsnetz will: Es tauscht die Ampel gegen Durchsatz. Ein Lauf, dessen Kamerabahn rot war, liefert trotzdem — und ist an seinen Messwerten als solcher zu erkennen.
Webhooks statt Pollen
Jeder Zustandswechsel als signierter POST. Pollen bleibt daneben immer möglich — ein Webhook ist die Ergänzung, kein Ersatz.
curl -X POST https://splatastic.com/api/v1/webhooks \
-H "Authorization: Bearer sk_…" -H "Content-Type: application/json" \
-d '{ "url": "https://dein-system.example/hooks/splatastic" }'
# → 201 { "id": "…", "url": "…", "secret": "…" } ← das Secret steht nur hier im Klartext{
"event": "capture.state",
"occurredAt": "2026-09-10T13:40:00.000Z",
"previousState": "processing",
"capture": {
"id": "cap_…", "name": "Halle 12", "clientRef": "acme:asset-42",
"flow": "headless",
"state": "needs-review",
"needsReview": {
"reason": "gate",
"text": "Die Kamerabahn wartet auf deine Prüfung.",
"url": "https://splatastic.com/aufnahmen/cap_…"
},
"error": null, "hasResult": false,
"createdAt": 1757500000000, "updatedAt": 1757500600000
}
}| Kopfzeile | Bedeutung |
|---|---|
X-Splatastic-Event | Das Ereignis dieser Zustellung: capture.state, capture.reexported oder capture.retention. Danach wird verzweigt — die Nutzlasten haben verschiedene Formen. |
X-Splatastic-Delivery | Kennung dieser Zustellung, stabil über alle Versuche — damit kannst du beim Empfangen entdoppeln. |
X-Splatastic-Timestamp | Unix-Sekunden dieses Versuchs; fließt in die Signatur ein. |
X-Splatastic-Signature | sha256=<hex HMAC-SHA256(secret, timestamp + "." + body)> |
const crypto = require('node:crypto')
function verify(secret, timestamp, rawBody, header) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
const a = Buffer.from(header)
const b = Buffer.from(expected)
// timingSafeEqual wirft bei ungleicher Länge, statt einfach false zu sein.
return a.length === b.length && crypto.timingSafeEqual(a, b)
}rawBody muss der unveränderte Request-Body sein, vor jedem JSON-Parsen, und timestamp der Wert aus X-Splatastic-Timestampdesselben Requests.
Antwortet dein Endpunkt nicht mit 2xx (oder gar nicht innerhalb von zehn Sekunden), wird wiederholt — nach etwa einer Minute, fünf Minuten, einer Viertelstunde, einer Stunde und sechs Stunden.X-Splatastic-Delivery bleibt dabei gleich, entdoppeln kannst du also darüber.
Verzweige immer über X-Splatastic-Event: Es gibt mehr als ein Ereignis, und die Nutzlasten haben verschiedene Formen. Ein Ereignis, das du nicht kennst, verwirfst du — dann bist du fertig, bevor es losgeht.
Der Liefervertrag
Was du bekommst, und was daran garantiert ist. Bricht ein Paket den Vertrag, wird es nicht ausgeliefert — statt einer Nutzlast, die dein Import nicht lesen kann.
| Feld | Wert | Bedeutung |
|---|---|---|
version | 1 | Die Fassung des Pakets. |
sourceType | "splatastic" | Woher das Paket kommt. |
upAxis | "Y" | Mehr als „Y zeigt nach oben": Die Lieferung ist zum Boden ausgerichtet. Ist die Schätzung unsicher — Treppe, Rampe, ein Gang, der nur eine Linie beschreibt —, bleibt die Szene so stehen, wie sie rekonstruiert wurde. |
metersPerUnit | 1 | Die Lieferung ist metrisch: eine Einheit ist ein Meter. Eine Aufnahme ganz ohne Maßstabsmessung ist ehrlich nicht metrisch und wird nicht ausgeliefert. |
identityTransform | true | Es ist nichts mehr umzurechnen — Splat und Kollisionsnetz tragen denselben Bake. |
| Datei | Inhalt |
|---|---|
splat.ply | Der Splat selbst. |
collision.glb | Das Kollisionsnetz, im selben Rahmen wie der Splat (auch als collision.ply). |
meta.json | Der Liefervertrag und die Kennzahlen des Laufs. |
package.zip | Alles zusammen in einer Datei. |
{
"deliveryId": "del_…",
"builtAt": 1757500600000,
"expiresAt": 1757504200000,
"files": {
"splatPly": { "url": "https://…", "filename": "splat.ply", "sizeBytes": 214748364 },
"collisionGlb": { "url": "https://…", "filename": "collision.glb", "sizeBytes": 5242880 },
"metaJson": { "url": "https://…", "filename": "meta.json", "sizeBytes": 512 },
"packageZip": { "url": "https://…", "filename": "package.zip", "sizeBytes": 225000000 }
},
"meta": { "version": 1, "metersPerUnit": 1, "upAxis": "Y", "…": "…" },
"metrics": { "psnr": 27.3, "splatCount": 812000, "…": "…" }
}Dazu trägt meta.json alles, was sonst noch bekannt ist: Herkunft, Qualitätsstufe, Kennzahlen des Laufs und unter areaCrop die Stufe, mit der die gelieferte Fassung auf den erfassten Bereich beschnitten wurde. Seit dem 17.09.2026 ist generous die Vorgabe— wer die volle Szene will, schickt beim Anlegen ausdrücklich "areaCrop": "off" mit. Was tatsächlich angewandt wurde, steht in jedem Paket in meta.json; das ist die verlässliche Angabe.
Die URLs aus …/result sind Abholscheine und laufen nach einer Stunde ab: Ein dauerhaft gültiger Link wäre ein Passwort, das nie abläuft. Kopier dir die Bytes; ein erneuter Aufruf liefert jederzeit frische URLs.
Fehlercodes
Eine Fehlerantwort hat immer die Form { "error": "<Text>", "code": "<Fehlercode>" } —error ist der Satz für Menschen, code das, was dein Programm abfragt.
| code | Wann |
|---|---|
validation | Eine Angabe im Aufruf fehlt oder ist unbekannt. |
unsupported_type | Dateityp nicht unterstützt — dazu gehören Rohdateien einer 360°-Kamera (.insv, .360). |
too_large | Größer als das Upload-Limit dieser Instanz. |
not_found | Standort, Aufnahme, Upload oder Webhook gibt es nicht — oder er gehört jemand anderem. |
no_object_store | Diese Instanz hat keinen Objektspeicher; ohne ihn gibt es keinen Direkt-Upload. |
stage_conflict | Die Aufnahme ist nicht (mehr) im passenden Schritt — ein Upload lässt sich nur einmal abschließen, und gelöscht wird nicht, während etwas läuft. |
not_running | cancel, obwohl gerade nichts läuft. |
not_delivered | GET …/result, bevor die Aufnahme ausgeliefert ist, oder das Paket fehlt. |
frame_stale | Die deliveryId eines schreibenden Aufrufs gehört zu einem früheren trainierten Stand. |
frame_mismatch | Die mitgeschickten bounds liegen zurückgerechnet weit neben der Szene. |
edit_conflict | Jemand hat seit deinem Stand gespeichert; die Antwort trägt den aktuellen Stand. |
reexport_running | Ein Re-Export dieser Aufnahme rechnet gerade. |
reexport_unavailable | Die Arbeitsdaten sind abgelaufen; ein erneuter Export wäre eine Neuberechnung aus dem Quellmaterial. |
invalid_url | Die Webhook-url ist keine gültige https://-Adresse. |
Du willst Splats aus deinem eigenen System bestellen?
Zugang anfragen