Upload your first file
Register a file and send its bytes directly to storage.
1. Prepare your file
Use a self-contained GLB, an OBJ, or a ZIP containing exactly one OBJ plus its MTL and PNG/JPEG textures. Include all appearance assets and preserve relative paths for accurate colors. Missing or unreadable OBJ materials/textures can use a default material or available base/vertex colors instead; inspection reports an appearance_fallback warning. File registration checks metadata; model processing validates contents.
Files must be 1–400,000,000 bytes. Encrypted ZIPs and links are unsupported. Export animated, skinned, morph-target or compressed GLB content as a static, self-contained model first.
2. Set credentials
Complete Get access and set API_BASE_URL and OTTOMFG_API_KEY in your server environment. Use a key with files:write.
3. Register and upload
Replace figurine.zip with your file. The examples calculate its size, register it, then send the bytes to storage. Send all returned form fields unchanged and the file field last; do not send your API key to storage.
Register and upload
// Node.js 22+. Set API_BASE_URL and OTTOMFG_API_KEY on your server.
import { randomUUID } from "node:crypto";
import { openAsBlob } from "node:fs";
import { stat } from "node:fs/promises";
import { basename } from "node:path";
const file = "figurine.zip";
const base = process.env.API_BASE_URL.replace(/\/$/, "");
// Generate once per intended registration; persist and reuse on retries.
const idempotencyKey = randomUUID();
const response = await fetch(`${base}/api/v1/files`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OTTOMFG_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ filename: basename(file), size_bytes: (await stat(file)).size }),
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
console.error(`Registration failed (${response.status}):`, await response.text());
process.exit(1); // Inspect code/message; do not blindly retry registration.
}
const registration = await response.json();
const form = new FormData();
for (const [name, value] of Object.entries(registration.upload.fields)) {
form.append(name, value);
}
form.append("file", await openAsBlob(file), basename(file)); // File must be last.
const transfer = await fetch(registration.upload.url, {
method: registration.upload.method,
body: form, // No API key or manual Content-Type header on the storage request.
signal: AbortSignal.timeout(3_600_000),
});
if (!transfer.ok) throw new Error(`Storage upload failed (${transfer.status})`);
console.log(registration.id);
Node.js 22+; cURL requires Bash, curl 7.76+, jq and uuidgen; Python requires requests (python -m pip install requests). File bytes are sent last, without your API key.
Storage returns HTTP 204 with an empty body after a successful upload. No upload-complete request is needed. This confirms transfer; model inspection checks geometry later.
4. Create a model
Save the file ID and create a model before the unlinked file expires. Upload instructions expire at upload.expires_at; see Limits for retention and upload windows.
These examples run on your server. Browser-direct uploads need an explicitly allowed origin in storage CORS; an arbitrary website origin is not supported. Keep API keys on your backend and pass only the short-lived upload instructions to an authorized browser.
API v1 preview