Files API
This API is currently in preview mode. Some routes are still subject to potential changes.
The Files API handles uploading, storing, and managing files for use across Nebul Inference API endpoints. Upload a document once, then reference it by ID in OCR, vision, and other model requests without re-uploading.
There are two upload methods: a simple single-request upload for small files, and a multipart upload for large files.
Base URL
https://api.inference.nebul.io
All endpoints require an Authorization header:
Authorization: Bearer <YOUR_API_KEY>
Simple Upload
POST /v1/files: Upload a file
Upload a file in a single request. Use this for files up to 50 MB.
POST /v1/filesContent-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Yes | The file to upload. |
purpose | String | Yes | Intended use. One of: assistants, batch, fine-tune, vision, user_data, evals, modelweights, logo, avatar, ocr. |
import httpxAPI_URL = "https://api.inference.nebul.io"HEADERS = {"Authorization": f"Bearer <YOUR_API_KEY>"}with open("document.pdf", "rb") as f:upload = httpx.post(f"{API_URL}/v1/files",files={"file": ("document.pdf", f, "application/pdf")},data={"purpose": "ocr"},headers=HEADERS,)file_id = upload.json()["id"]
curl -X POST https://api.inference.nebul.io/v1/files \-H "Authorization: Bearer <YOUR_API_KEY>" \-F "file=@document.pdf" \-F "purpose=ocr"
Response:
{"id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b","filename": "document.pdf","org_id": "0191a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b","project_id": "0192a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b","purpose": "ocr","file_size": 245678,"status": "complete","created_at": "2025-01-15T10:30:00Z"}
GET /v1/files: List files
List files you have uploaded. Optional query parameters filter the results.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
purpose | query | String | No | Filter by file purpose (e.g. ocr, vision). |
created_by | query | UUID | No | Filter by the user who uploaded the file. |
files = httpx.get(f"{API_URL}/v1/files",params={"purpose": "ocr"},headers=HEADERS,).json()
curl https://api.inference.nebul.io/v1/files?purpose=ocr \-H "Authorization: Bearer <YOUR_API_KEY>"
Response:
{"object": "list","data": [{"id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b","filename": "document.pdf","purpose": "ocr","file_size": 245678,"status": "complete","created_at": "2025-01-15T10:30:00Z"}]}
GET /v1/files/{file_id}: Retrieve file metadata
Get metadata for a specific file by its ID.
file_meta = httpx.get(f"{API_URL}/v1/files/{file_id}",headers=HEADERS,).json()
curl https://api.inference.nebul.io/v1/files/0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b \-H "Authorization: Bearer <YOUR_API_KEY>"
GET /v1/files/{file_id}/content: Download file content
Download the raw file content. Returns the file bytes directly.
content = httpx.get(f"{API_URL}/v1/files/{file_id}/content",headers=HEADERS,)with open("downloaded.pdf", "wb") as f:f.write(content.content)
curl https://api.inference.nebul.io/v1/files/0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b/content \-H "Authorization: Bearer <YOUR_API_KEY>" \--output downloaded.pdf
DELETE /v1/files/{file_id}: Delete a file
Remove a file and its stored content. Returns a confirmation with the deleted file ID.
httpx.delete(f"{API_URL}/v1/files/{file_id}",headers=HEADERS,)
curl -X DELETE https://api.inference.nebul.io/v1/files/0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b \-H "Authorization: Bearer <YOUR_API_KEY>"
Response:
{"id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b","object": "file","deleted": true}
Multipart Upload
For files larger than 50 MB, use the multipart upload flow. This splits the upload into parts that can be sent independently and reassembled server-side.
Step 1: Create an upload session
POST /v1/uploadsContent-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
filename | String | Yes | Name of the file to upload. |
purpose | String | Yes | Intended use (same values as simple upload). |
bytes | Integer | Yes | Total file size in bytes. |
upload_session = httpx.post(f"{API_URL}/v1/uploads",json={"filename": "large-document.pdf","purpose": "ocr","bytes": 524288000,},headers={**HEADERS, "Content-Type": "application/json"},).json()upload_id = upload_session["id"]
curl -X POST https://api.inference.nebul.io/v1/uploads \-H "Authorization: Bearer <YOUR_API_KEY>" \-H "Content-Type: application/json" \-d '{"filename": "large-document.pdf", "purpose": "ocr", "bytes": 524288000}'
Response:
{"id": "0194a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b","object": "upload","bytes": 524288000,"bytes_uploaded": 0,"created_at": "2025-01-15T10:30:00Z","status": "pending","filename": "large-document.pdf","purpose": "ocr"}
Step 2: Upload parts
Upload each chunk of the file as a separate part.
POST /v1/uploads/{upload_id}/partsContent-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
part_number | Integer | Yes | 1-based index of this part. |
data | File | Yes | The raw bytes for this part. |
part = httpx.post(f"{API_URL}/v1/uploads/{upload_id}/parts",files={"data": ("part", chunk_bytes, "application/octet-stream")},data={"part_number": str(part_number)},headers=HEADERS,).json()part_ids.append(part["id"])
curl -X POST https://api.inference.nebul.io/v1/uploads/$UPLOAD_ID/parts \-H "Authorization: Bearer <YOUR_API_KEY>" \-F "part_number=1" \-F "data=@part1.bin"
Response:
{"id": "part-abc123","object": "upload.part","created_at": "2025-01-15T10:31:00Z","upload_id": "0194a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"}
Step 3: Complete the upload
Once all parts are uploaded, finalize the upload by providing the ordered list of part IDs.
POST /v1/uploads/{upload_id}/completeContent-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
part_ids | String[] | Yes | Ordered list of part IDs from step 2. |
httpx.post(f"{API_URL}/v1/uploads/{upload_id}/complete",json={"part_ids": part_ids},headers={**HEADERS, "Content-Type": "application/json"},)
curl -X POST https://api.inference.nebul.io/v1/uploads/$UPLOAD_ID/complete \-H "Authorization: Bearer <YOUR_API_KEY>" \-H "Content-Type: application/json" \-d '{"part_ids": ["part-abc123", "part-def456"]}'
Cancel an upload
Abort an in-progress multipart upload. Already-uploaded parts are discarded.
httpx.post(f"{API_URL}/v1/uploads/{upload_id}/cancel",headers=HEADERS,)
Check upload status
status = httpx.get(f"{API_URL}/v1/uploads/{upload_id}",headers=HEADERS,).json()
Upload status values: pending, in_progress, completed, cancelled, failed.
File Object
Every file operation returns a file object with these fields:
| Field | Type | Description |
|---|---|---|
id | UUID | Unique file identifier. |
filename | String | Original filename. |
org_id | UUID | Organization that owns the file. |
project_id | UUID | Project the file belongs to. |
purpose | String | Why the file was uploaded. |
file_size | Integer | Size in bytes. |
checksum | String | SHA-256 checksum (null during upload). |
status | String | Lifecycle state: pending, complete, cancelled, failed, deleted. |
created_at | DateTime | Upload timestamp. |
Using Files with OCR
After uploading a file, pass the file_id to the OCR endpoint:
result = httpx.post(f"{API_URL}/v1/files/{file_id}/ocr",json={"model": "deepseek-ai/DeepSeek-OCR"},headers={**HEADERS, "Content-Type": "application/json"},timeout=120.0,).json()
See the OCR page for the full OCR API reference.
Errors
| Status | Cause |
|---|---|
| 400 | Invalid input: unsupported format, missing required field |
| 401 | Missing or invalid API key |
| 404 | File or upload not found |
| 413 | File exceeds maximum size |
| 415 | Unsupported file type |
| 422 | Validation error in request parameters |
Troubleshooting
422 with "Field required" on upload
If you get a 422 error where both file and purpose are reported as missing, the most likely cause is a manually set Content-Type header overriding the auto-generated multipart/form-data boundary.
When using httpx (or similar clients), the files parameter automatically sets the correct Content-Type: multipart/form-data; boundary=.... If your default headers include Content-Type: application/json, it overrides this boundary and the server cannot parse the body.
# Wrong: Content-Type: application/json overwrites the multipart boundaryresponse = await client.post(f"{API_URL}/v1/files",files={"file": (filename, file_bytes, file_type)},data={"purpose": "ocr"},headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}, # ← breaks multipart)# Correct: omit Content-Type, let httpx set itresponse = await client.post(f"{API_URL}/v1/files",files={"file": (filename, file_bytes, file_type)},data={"purpose": "ocr"},headers={"Authorization": f"Bearer {API_KEY}"},)
If you use a shared headers dict, filter out Content-Type for multipart requests:
headers = {k: v for k, v in default_headers.items() if k.lower() != "content-type"}