Nebul Docs

Files API

Upload and manage files once, then reference them by ID across OCR, vision, and other Inference API requests

PREVIEW MODE

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 $NEBUL_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/files
Content-Type: multipart/form-data
FieldTypeRequiredDescription
fileFileYesThe file to upload.
purposeStringYesIntended use. One of: assistants, batch, fine-tune, vision, user_data, evals, modelweights, logo, avatar, ocr.
import os

import httpx

API_URL = "https://api.inference.nebul.io"
HEADERS = {"Authorization": f"Bearer {os.environ['NEBUL_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 $NEBUL_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.

ParameterInTypeRequiredDescription
purposequeryStringNoFilter by file purpose (e.g. ocr, vision).
created_byqueryUUIDNoFilter 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 $NEBUL_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 $NEBUL_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 $NEBUL_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 $NEBUL_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/uploads
Content-Type: application/json
FieldTypeRequiredDescription
filenameStringYesName of the file to upload.
purposeStringYesIntended use (same values as simple upload).
bytesIntegerYesTotal 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 $NEBUL_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}/parts
Content-Type: multipart/form-data
FieldTypeRequiredDescription
part_numberIntegerYes1-based index of this part.
dataFileYesThe 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 $NEBUL_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}/complete
Content-Type: application/json
FieldTypeRequiredDescription
part_idsString[]YesOrdered 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 $NEBUL_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:

FieldTypeDescription
idUUIDUnique file identifier.
filenameStringOriginal filename.
org_idUUIDOrganization that owns the file.
project_idUUIDProject the file belongs to.
purposeStringWhy the file was uploaded.
file_sizeIntegerSize in bytes.
checksumStringSHA-256 checksum (null during upload).
statusStringLifecycle state: pending, complete, cancelled, failed, deleted.
created_atDateTimeUpload 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

StatusCause
400Invalid input: unsupported format, missing required field
401Missing or invalid API key
404File or upload not found
413File exceeds maximum size
415Unsupported file type
422Validation 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 boundary
response = 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 it
response = 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"}

On this page