Uploads API
Push model weights into the platform three ways: chunked single-file uploads with resume and SHA256 checks, directory manifests with per-file chunking, or a tar archive the server unpacks server-side.
The Uploads API exposes three modes:
- Single chunked upload. Open a session, push each chunk with a SHA256 checksum, then call complete. Interrupted runs can resume.
- Directory upload. Provide a file manifest at session creation. Each file is uploaded individually. Large files in the manifest are chunked automatically.
- Archive upload. Upload a tar, tar.gz, or tar.bz2 archive. The server extracts it after the session completes.
Sessions expire after 24 hours. Incomplete sessions in
pending or uploading state are cleaned up on expiry.
Track expires_at on the session and call
Resume Upload before it lapses to recover an interrupted run.
All upload modes support resumability, SHA256 integrity verification, and automatic model record creation on completion. Uploaded content is stored using the platform's two-tier storage architecture. For details on storage tiers, encryption, retention, and billing, see Storage.
Method Index
Every endpoint on this page in scan order. Click a row to jump.
| Method | Path | Purpose |
|---|---|---|
| POST | /:project_id/v1/uploads | Open a single-file chunked upload session. |
| GET | /:project_id/v1/uploads | List sessions for the authenticated project. |
| GET | /:project_id/v1/uploads/{upload_id} | Read status, progress, and chunk count for a session. |
| POST | /:project_id/v1/uploads/{upload_id}/parts | Upload one chunk with a SHA256 checksum. |
| POST | /:project_id/v1/uploads/{upload_id}/complete | Finalize the session. Creates the model record. |
| POST | /:project_id/v1/uploads/{upload_id}/cancel | Cancel an in-progress session, drop uploaded chunks. |
| DELETE | /:project_id/v1/uploads/{upload_id} | Delete a session. Same effect as cancel. |
| POST | /:project_id/v1/uploads/{upload_id}/resume | Discover the next expected chunk and any gaps. |
| POST | /:project_id/v1/uploads/directory | Open a multi-file directory session with manifest. |
| POST | /:project_id/v1/uploads/{upload_id}/files/{relative_path} | Upload one small file in directory mode. |
| POST | /:project_id/v1/uploads/{upload_id}/file-chunks/{chunk_index} | Upload one chunk of a large file in directory mode. |
| POST | /:project_id/v1/uploads/{upload_id}/file-complete | Assemble chunks for one file in directory mode. |
| POST | /:project_id/v1/uploads/archive | Open an archive upload session for server-side extraction. |
Create Upload
POST /:project_id/v1/uploads
Creates a new chunked upload session for a single file.
Request Body
| Parameter | Type | Description |
|---|---|---|
| purposerequired | string | The intended purpose. Currently only "model" is supported. |
| filenamerequired | string | Name of the file being uploaded. |
| bytesrequired | integer | Total file size in bytes. Must be greater than 0. |
| mime_typeoptional | string | MIME type of the file. Defaults to "application/octet-stream". |
curl -X POST https://api.xerotier.ai/proj_ABC123/v1/uploads \
-H "Authorization: Bearer xero_myproject_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"purpose": "model",
"filename": "model.safetensors",
"bytes": 10737418240,
"mime_type": "application/octet-stream"
}'
import requests
base = "https://api.xerotier.ai/proj_ABC123/v1"
headers = {"Authorization": "Bearer xero_myproject_your_api_key"}
upload = requests.post(f"{base}/uploads", headers=headers, json={
"purpose": "model",
"filename": "model.safetensors",
"bytes": 10737418240
}).json()
const base = "https://api.xerotier.ai/proj_ABC123/v1";
const headers = {
"Authorization": "Bearer xero_myproject_your_api_key",
"Content-Type": "application/json"
};
const upload = await fetch(`${base}/uploads`, {
method: "POST",
headers,
body: JSON.stringify({
purpose: "model",
filename: "model.safetensors",
bytes: 10737418240
})
}).then(r => r.json());
Response
{
"id": "00000000-1111-0000-1111-000000000000",
"object": "upload",
"bytes": 10737418240,
"created_at": 1700000000,
"filename": "model.safetensors",
"purpose": "model",
"status": "pending",
"expires_at": 1700086400,
"upload_type": "single",
"chunk_size": 104857600,
"total_chunks": 103,
"uploaded_chunks": 0,
"progress": 0
}
List Uploads
GET /:project_id/v1/uploads
Lists upload sessions for the authenticated project.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| limitoptional | integer | Maximum number of sessions to return (1-100, default 20). |
| statusoptional | string | Filter by upload status (e.g., "pending", "completed", "cancelled"). |
curl https://api.xerotier.ai/proj_ABC123/v1/uploads?limit=10 \
-H "Authorization: Bearer xero_myproject_your_api_key"
Response
{
"object": "list",
"data": [
{
"id": "00000000-1111-0000-1111-000000000000",
"object": "upload",
"bytes": 10737418240,
"created_at": 1700000000,
"filename": "model.safetensors",
"purpose": "model",
"status": "pending",
"expires_at": 1700086400,
"upload_type": "single",
"chunk_size": 104857600,
"total_chunks": 103,
"uploaded_chunks": 50,
"progress": 48.54
}
],
"first_id": "00000000-1111-0000-1111-000000000000",
"last_id": "00000000-1111-0000-1111-000000000000",
"has_more": false
}
Get Upload
GET /:project_id/v1/uploads/{upload_id}
Retrieves the current status and progress of an upload session.
curl https://api.xerotier.ai/proj_ABC123/v1/uploads/00000000-1111-0000-1111-000000000000 \
-H "Authorization: Bearer xero_myproject_your_api_key"
Response
{
"id": "00000000-1111-0000-1111-000000000000",
"object": "upload",
"bytes": 10737418240,
"created_at": 1700000000,
"filename": "model.safetensors",
"purpose": "model",
"status": "pending",
"expires_at": 1700086400,
"upload_type": "single",
"chunk_size": 104857600,
"total_chunks": 103,
"uploaded_chunks": 75,
"progress": 72.82
}
Add Part
POST /:project_id/v1/uploads/{upload_id}/parts
Uploads a single chunk (part) to an active upload session. Each chunk must include a SHA256 checksum for integrity verification.
Chunk size ceiling. The server rejects any single chunk
body larger than 200 MB with 413 content_too_large. Read
chunk_size from the create response and stream exactly that
many bytes per part. The final part may be smaller.
Headers
| Header | Description |
|---|---|
| X-Chunk-Checksumrequired | SHA256 hex digest of the chunk data. |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| part_numberrequired | integer | Zero-based chunk index. Can also be provided via X-Part-Number header. |
The request body is the raw binary chunk data. Maximum chunk size is 200 MB.
curl -X POST "https://api.xerotier.ai/proj_ABC123/v1/uploads/00000000-1111-0000-1111-000000000000/parts?part_number=0" \
-H "Authorization: Bearer xero_myproject_your_api_key" \
-H "X-Chunk-Checksum: a1b2c3d4e5f6..." \
--data-binary @chunk_000.bin
Response
{
"id": "part_0",
"object": "upload.part",
"created_at": 1700000100,
"upload_id": "00000000-1111-0000-1111-000000000000",
"chunk_index": 0,
"bytes_received": 104857600,
"checksum": "a1b2c3d4e5f6..."
}
Complete Upload
POST /:project_id/v1/uploads/{upload_id}/complete
Marks the upload as complete. All chunks must have been uploaded before calling this endpoint. For archive uploads, the server extracts the archive. For directory uploads, all expected files must be uploaded. On success, a model record is created automatically.
curl -X POST https://api.xerotier.ai/proj_ABC123/v1/uploads/00000000-1111-0000-1111-000000000000/complete \
-H "Authorization: Bearer xero_myproject_your_api_key"
Response
{
"id": "00000000-1111-0000-1111-000000000000",
"object": "upload",
"status": "completed",
"bytes": 10737418240,
"created_at": 1700000000,
"filename": "model.safetensors",
"upload_type": "single",
"model": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "model.safetensors",
"format": "safetensors",
"size_bytes": 10737418240,
"status": "ready",
"context_length": 4096,
"architecture": "llama",
"quantization": null
}
}
Cancel Upload
POST /:project_id/v1/uploads/{upload_id}/cancel
Cancels an in-progress upload session and cleans up all uploaded chunks.
curl -X POST https://api.xerotier.ai/proj_ABC123/v1/uploads/00000000-1111-0000-1111-000000000000/cancel \
-H "Authorization: Bearer xero_myproject_your_api_key"
Response
{
"id": "00000000-1111-0000-1111-000000000000",
"object": "upload",
"bytes": 10737418240,
"created_at": 1700000000,
"filename": "model.safetensors",
"purpose": "model",
"status": "cancelled",
"expires_at": 1700086400
}
Delete Upload
DELETE /:project_id/v1/uploads/{upload_id}
Deletes an upload session. This is functionally identical to cancelling the upload.
curl -X DELETE https://api.xerotier.ai/proj_ABC123/v1/uploads/00000000-1111-0000-1111-000000000000 \
-H "Authorization: Bearer xero_myproject_your_api_key"
Resume Upload
POST /:project_id/v1/uploads/{upload_id}/resume
Retrieves information needed to resume an interrupted upload. Returns the next expected chunk index and a list of any missing chunks that need to be re-uploaded.
curl -X POST https://api.xerotier.ai/proj_ABC123/v1/uploads/00000000-1111-0000-1111-000000000000/resume \
-H "Authorization: Bearer xero_myproject_your_api_key"
Response
{
"id": "00000000-1111-0000-1111-000000000000",
"next_chunk_index": 52,
"uploaded_chunks": 50,
"missing_chunks": [5, 10]
}
Directory Uploads
POST /:project_id/v1/uploads/directory
Creates a directory upload session for uploading an entire model directory (multiple files). You provide a file manifest describing all files, and the server returns per-file upload paths.
Request Body
| Parameter | Type | Description |
|---|---|---|
| model_namerequired | string | Name for the model being uploaded. |
| filesrequired | array | Array of file manifest entries, each with relative_path (string) and size (integer, bytes). |
| descriptionoptional | string | Description of the model. |
| workload_typeoptional | string | Workload type hint for the model. |
| quantizationoptional | string | Runtime quantization method to apply. |
curl -X POST https://api.xerotier.ai/proj_ABC123/v1/uploads/directory \
-H "Authorization: Bearer xero_myproject_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model_name": "Qwen3-0.6B",
"files": [
{"relative_path": "config.json", "size": 1234},
{"relative_path": "model.safetensors", "size": 1234000000}
]
}'
Response
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"object": "upload",
"bytes": 1234001234,
"created_at": 1700000000,
"filename": "Qwen3-0.6B",
"purpose": "model",
"status": "pending",
"expires_at": 1700086400,
"upload_type": "directory",
"chunk_size": 104857600,
"uploaded_chunks": 0,
"progress": 0,
"files": [
{
"relative_path": "config.json",
"upload_path": "v1/uploads/770e8400-.../files/config.json",
"size": 1234,
"requires_chunking": false,
"total_chunks": 0,
"status": "pending"
},
{
"relative_path": "model.safetensors",
"upload_path": "v1/uploads/770e8400-.../files/model.safetensors",
"size": 1234000000,
"requires_chunking": true,
"total_chunks": 12,
"chunk_url": "v1/uploads/770e8400-.../file-chunks",
"status": "pending"
}
],
"chunk_upload_url": "v1/uploads/770e8400-.../file-chunks"
}
Upload Individual Files
POST /:project_id/v1/uploads/{upload_id}/files/{relative_path}
Uploads a single file (for small files that do not require chunking). Include the X-Chunk-Checksum header with the SHA256 hex digest.
curl -X POST https://api.xerotier.ai/proj_ABC123/v1/uploads/770e8400-.../files/config.json \
-H "Authorization: Bearer xero_myproject_your_api_key" \
-H "X-Chunk-Checksum: abc123..." \
--data-binary @config.json
Upload File Chunks
POST /:project_id/v1/uploads/{upload_id}/file-chunks/{chunk_index}?relative_path={path}
Uploads a single chunk of a large file within a directory upload. The relative_path query parameter identifies which file the chunk belongs to.
Complete File
POST /:project_id/v1/uploads/{upload_id}/file-complete
Assembles chunks for a file in directory mode. Send a JSON body with "relative_path" to identify the file.
Archive Uploads
POST /:project_id/v1/uploads/archive
Creates an archive upload session. Upload a compressed archive containing the model files. The archive is extracted server-side when the upload completes.
Request Body
| Parameter | Type | Description |
|---|---|---|
| model_namerequired | string | Name for the model being uploaded. |
| archive_sizerequired | integer | Total archive size in bytes. Must be greater than 0. |
| archive_formatrequired | string | Archive format. Supported: "tar", "tar.gz", "tar.bz2". |
| descriptionoptional | string | Description of the model. |
| workload_typeoptional | string | Workload type hint for the model. |
| quantizationoptional | string | Runtime quantization method to apply. |
curl -X POST https://api.xerotier.ai/proj_ABC123/v1/uploads/archive \
-H "Authorization: Bearer xero_myproject_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model_name": "Qwen3-0.6B",
"archive_size": 1234567890,
"archive_format": "tar.gz"
}'
Response
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"object": "upload",
"bytes": 1234567890,
"created_at": 1700000000,
"filename": "Qwen3-0.6B",
"purpose": "model",
"status": "pending",
"expires_at": 1700086400,
"upload_type": "archive",
"chunk_size": 104857600,
"total_chunks": 12,
"uploaded_chunks": 0,
"progress": 0
}
After creating the archive upload session, upload chunks using the Add Part endpoint, then call Complete Upload to trigger server-side extraction.
Status Lifecycle
Upload sessions transition through the following states:
| Status | Description |
|---|---|
pending | Session created, waiting for chunks to be uploaded. |
uploading | Chunks are actively being uploaded. |
completed | Upload finished successfully. A model record has been created. |
cancelled | Upload was cancelled via the cancel or delete endpoint; uploaded chunks are cleaned up. |
expired | Session expired (24 hours) before completion. |
Sessions expire automatically after 24 hours. Incomplete sessions in
pending or uploading state are cleaned up on expiry.
Use the Resume Upload endpoint before a session expires
to recover interrupted uploads.
Client Examples
Python (requests)
import requests
import hashlib
headers = {
"Authorization": "Bearer xero_myproject_your_api_key",
"Content-Type": "application/json"
}
base = "https://api.xerotier.ai/proj_ABC123/v1"
# Step 1: Create an upload session
upload = requests.post(f"{base}/uploads", headers=headers, json={
"purpose": "model",
"filename": "model.safetensors",
"bytes": 10737418240,
"mime_type": "application/octet-stream"
}).json()
print(f"Upload created: {upload['id']}")
print(f"Chunk size: {upload['chunk_size']}, Total chunks: {upload['total_chunks']}")
# Step 2: Upload chunks
chunk_size = upload["chunk_size"]
with open("model.safetensors", "rb") as f:
part_number = 0
while True:
data = f.read(chunk_size)
if not data:
break
checksum = hashlib.sha256(data).hexdigest()
requests.post(
f"{base}/uploads/{upload['id']}/parts?part_number={part_number}",
headers={
"Authorization": headers["Authorization"],
"X-Chunk-Checksum": checksum
},
data=data
)
part_number += 1
print(f"Uploaded chunk {part_number}/{upload['total_chunks']}")
# Step 3: Complete the upload
result = requests.post(
f"{base}/uploads/{upload['id']}/complete",
headers=headers
).json()
print(f"Upload complete: {result['status']}")
# List uploads
uploads = requests.get(f"{base}/uploads?limit=10", headers=headers).json()
for u in uploads["data"]:
print(f"{u['id']} - {u['filename']} ({u['status']}, {u['progress']}%)")
# Resume an interrupted upload
resume = requests.post(
f"{base}/uploads/{upload['id']}/resume",
headers=headers
).json()
print(f"Next chunk: {resume['next_chunk_index']}, Missing: {resume['missing_chunks']}")
Node.js (fetch)
import { readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
const base = "https://api.xerotier.ai/proj_ABC123/v1";
const headers = {
"Authorization": "Bearer xero_myproject_your_api_key",
"Content-Type": "application/json"
};
// Step 1: Create an upload session
const createRes = await fetch(`${base}/uploads`, {
method: "POST",
headers,
body: JSON.stringify({
purpose: "model",
filename: "model.safetensors",
bytes: 10737418240,
mime_type: "application/octet-stream"
})
});
const upload = await createRes.json();
console.log(`Upload created: ${upload.id}`);
// Step 2: Upload chunks
const fileData = await readFile("model.safetensors");
const chunkSize = upload.chunk_size;
for (let i = 0; i < upload.total_chunks; i++) {
const start = i * chunkSize;
const chunk = fileData.subarray(start, start + chunkSize);
const checksum = createHash("sha256").update(chunk).digest("hex");
await fetch(`${base}/uploads/${upload.id}/parts?part_number=${i}`, {
method: "POST",
headers: {
"Authorization": headers.Authorization,
"X-Chunk-Checksum": checksum
},
body: chunk
});
console.log(`Uploaded chunk ${i + 1}/${upload.total_chunks}`);
}
// Step 3: Complete the upload
const completeRes = await fetch(`${base}/uploads/${upload.id}/complete`, {
method: "POST",
headers
});
const result = await completeRes.json();
console.log(`Upload complete: ${result.status}`);
// List uploads
const listRes = await fetch(`${base}/uploads?limit=10`, { headers });
const uploads = await listRes.json();
uploads.data.forEach(u =>
console.log(`${u.id} - ${u.filename} (${u.progress}%)`)
);
Error Handling
| HTTP Status | Error Code | Cause | Remediation |
|---|---|---|---|
| 400 | invalid_request |
Missing or invalid parameters (bad filename, zero bytes, invalid archive format, missing checksum). | Validate the request body locally. Confirm purpose, filename, bytes > 0, and the X-Chunk-Checksum header are present. |
| 401 | authentication_error |
Invalid or missing API key. | Send Authorization: Bearer xero_<project>_<key>. Rotate the key if compromised. |
| 403 | forbidden |
Free tier model limit exceeded or storage quota exceeded. | Delete an existing model, raise the tier, or wait for the quota window to roll over. |
| 404 | not_found |
Upload session not found or expired. | Sessions expire after 24 hours. Start a new session and re-upload. |
| 413 | content_too_large |
Individual chunk exceeds the 200 MB ceiling. | Read chunk_size from the create response and stream exactly that many bytes per part. |
| 429 | rate_limit_exceeded |
Too many requests. | Back off using the Retry-After header. Cap concurrent chunk uploads at 4 to 8. |
| 503 | service_unavailable |
Storage service temporarily unavailable. | Retry with exponential backoff. Call Resume Upload to pick up where you left off. |