API v1 preview

# Create a file

`POST /api/v1/files`

## Overview

Create a file record and receive temporary upload instructions. Upload the bytes to the returned URL using the supplied form fields. Registration does not upload or validate the model. Idempotency-Key is optional; successful retries return the original snapshot for 24 hours.

Requires `files:write`.

[Upload guide: transfer and retention](https://ottomfg.com/docs/quickstart.md)

[Errors and retries](https://ottomfg.com/docs/errors.md)

Request

cURL

```
curl -X POST "$API_BASE_URL/api/v1/files" \
  -H "Authorization: Bearer $OTTOMFG_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "filename": "figurine.zip",
  "size_bytes": 10485760
}'
```

Use your configured API_BASE_URL and the signed upload fields returned by your request.

**More request examples**

Request body

```
{
  "filename": "figurine.zip",
  "size_bytes": 10485760
}
```

**Complete upload examples**

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.

## Parameters

`Idempotency-Key` header · optional

Account, method and path scoped retry key, retained for 24 hours from first use. Same body replays the original successful status and body. A changed body returns 409 idempotency_key_reused; an in-flight request returns 409 idempotency_request_in_progress with Retry-After. Retry transient failures with the same key and body.

string

## Request body

application/json

`filename` string · required

Original filename, including .glb, .obj, or .zip. Maximum 255 characters; directory paths, leading/trailing whitespace, and control characters are not allowed.

Min length: 1 · Max length: 255

`size_bytes` integer (int64) · required

Exact file size in bytes. For ZIPs, use the compressed size. Maximum 400000000 bytes (400 MB).

Minimum: 1 · Maximum: 400000000

## Responses

File registered; upload the bytes using the returned instructions.

200 response

JSON

```
{
  "upload": {
    "method": "POST",
    "url": "https://example-upload-bucket.s3.amazonaws.com/",
    "fields": {
      "key": "files/2a08c3d391244972a31a0f87d3821fa9/source",
      "policy": "GENERATED_POLICY",
      "x-amz-algorithm": "AWS4-HMAC-SHA256",
      "x-amz-credential": "GENERATED_CREDENTIAL_SCOPE",
      "x-amz-date": "20260921T180000Z",
      "x-amz-signature": "GENERATED_SIGNATURE",
      "x-amz-security-token": "GENERATED_SESSION_TOKEN",
      "success_action_status": "204"
    },
    "expires_at": "2026-09-21T19:00:00Z"
  },
  "id": "file_6c82bd3e319b4bb099310d54a761879e"
}
```

Returns [the File object](https://ottomfg.com/docs/file-object.md).

**Response headers**

`X-Request-ID`

Request identifier for support. Send X-Request-ID with 8–64 ASCII letters, digits or hyphens to reuse that value; otherwise the server generates a UUID4 hex value. Include the returned X-Request-ID in support requests. Do not put secrets or account IDs in this value.

`Idempotent-Replayed`

Present when the original successful response is replayed.

## Errors

| HTTP | Code | What to do |
| --- | --- | --- |
| 400 Bad Request | `file_too_large` | The file is over the upload limit. Reduce it before you register it. |
| 400 Bad Request | `filename_required` | The file registration has no filename. Send the original filename with its extension. |
| 400 Bad Request | `invalid_argument` | A field, query parameter or header is invalid. Fix the field named in details, then send the request again. |
| 400 Bad Request | `invalid_file_size` | size_bytes is not a positive whole number. Send the file's exact byte count. |
| 400 Bad Request | `size_bytes_required` | The file registration has no size_bytes. Send the file's exact byte count. |
| 400 Bad Request | `unsupported_file_extension` | The file type is not supported. Use a GLB, OBJ or ZIP file. |
| 401 Unauthorized | `unauthenticated` | The API key is missing, invalid or revoked. Send a valid business API key in Authorization: Bearer. |
| 403 Forbidden | `permission_denied` | The key lacks the endpoint's scope, or payments lack spending consent. Check the scope listed on the endpoint. |
| 409 Conflict | `idempotency_key_reused` | This Idempotency-Key was already used with a different body. Retry the original body, or use a new key for a new operation. |
| 409 Conflict | `idempotency_request_in_progress` | A request with this Idempotency-Key is still running. Wait Retry-After seconds, then retry the same key and body. |
| 413 Content Too Large | `request_too_large` | The JSON body is over this endpoint's size limit. Send a smaller body. |
| 415 Unsupported Media Type | `unsupported_media_type` | The body is not JSON. Send Content-Type: application/json. |
| 429 Too Many Requests | `resource_exhausted` | A rate or account limit was reached. Wait at least Retry-After seconds, then retry. |
| 500 Internal Server Error | `internal` | The server failed unexpectedly. Retry with backoff; contact support with X-Request-ID if it persists. |
| 503 Service Unavailable | `unavailable` | A required service is briefly unavailable. Retry with backoff, reusing the same Idempotency-Key. |

[Idempotent requests](https://ottomfg.com/docs/idempotency.md)

API v1 preview

Source: https://ottomfg.com/docs/create-file
