Upload files
Send a file of up to 1 GB in parts, then follow it until it is searchable.
You can add a document in one of two ways, depending on the file.
| The file is | Use | Calls |
|---|---|---|
| Text, up to 256 KiB | A small text document | One request. See Small text documents. |
| A PDF, an Office file, or any other file type ByteRover reads, up to 1 GB | An upload | Start, send the parts, complete. Covered on this page. |
To load a whole folder, follow the recipe Upload a folder and search it.
Before you start
- An API key with the Read and write scope that can reach the drive. See Authentication and API keys.
- The drive ID.
GET /v1/driveslists the drives your key can reach.
How an upload works
Start the upload
Call POST /v1/drives/{driveId}/uploads with the file's path, size and SHA-256. The response is an upload plan that gives the part size, the number of parts, and an address for each part.
Send each part
PUT each part's bytes to its address. Parts go directly to storage and bypass the API. Save the ETag header from each response.
Complete the upload
Call POST /v1/uploads/{uploadId}/complete with every part number and its ETag. The response contains the document and an operation.
Follow the operation
Poll GET /v1/operations/{operationId} until its status is done. At that point the document is searchable.
The full example
import hashlib
import os
import time
import requests
API = "https://byterover.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['BYTEROVER_API_KEY']}"}
def call(method, url, **kwargs):
response = requests.request(method, url, headers=HEADERS, **kwargs)
response.raise_for_status()
return response.json()
def upload(drive_id, local_path, path):
# 1. Start: the path in the drive, the size and the SHA-256 of the file.
digest = hashlib.sha256()
with open(local_path, "rb") as file:
for block in iter(lambda: file.read(1024 * 1024), b""):
digest.update(block)
plan = call("POST", f"{API}/drives/{drive_id}/uploads", json={
"path": path,
"size_bytes": os.path.getsize(local_path),
"sha256": digest.hexdigest(),
})
# 2. Send each part to its address, and keep its ETag.
sent = []
parts, cursor = plan["parts"], plan["next_cursor"]
with open(local_path, "rb") as file:
while True:
for part in parts:
file.seek((part["part_number"] - 1) * plan["part_size"])
put = requests.put(part["url"], data=file.read(plan["part_size"]))
put.raise_for_status()
sent.append({"part_number": part["part_number"], "etag": put.headers["ETag"]})
if cursor is None:
break
page = call("GET", f"{API}/uploads/{plan['upload_id']}/parts", params={"cursor": cursor})
parts, cursor = page["parts"], page["next_cursor"]
# 3. Complete: the file becomes a document, and an operation starts.
written = call("POST", f"{API}/uploads/{plan['upload_id']}/complete", json={"parts": sent})
# 4. Follow the operation until the document is searchable.
operation = written["operation"]
while operation["status"] in ("queued", "running"):
time.sleep((operation["retry_after_ms"] or 1000) / 1000)
operation = call("GET", f"{API}/operations/{operation['id']}")
if operation["status"] == "failed":
raise RuntimeError(operation["error"]["message"])
return written["document"]
document = upload("drv_4h7k2m9q8r1t5v6w3x0y", "customer-refund-policy.pdf", "/policies/customer-refund-policy.pdf")
print(document["id"])Facts about an upload
| Fact | Value |
|---|---|
| Largest file | 1 GB |
| Part size | 8 MiB. Every part is this size except the last, which can be smaller. Use part_size from the plan. |
| Addresses per response | 100. If next_cursor isn't null, fetch the next page with GET /v1/uploads/{uploadId}/parts?cursor=. |
| Address lifetime | 15 minutes (see expires_at). Use the same call to get fresh addresses. |
| Upload lifetime | 24 hours. After that the upload closes, the API returns UPLOAD_CLOSED, and you need to start a new one. |
| Calling complete again | Returns the result of the first call, so retries are safe. |
| SHA-256 | Lowercase hex digest of the whole file. ByteRover checks it again before reading the file, and the document fails if it doesn't match. |
The path decides the document
path is the document's location in the drive, including its name, for example /policies/customer-refund-policy.pdf. The name must end in a file type that ByteRover reads.
If no document exists at the path, ByteRover creates one. If a document already exists there, the upload adds a new revision to it, and searches read the new revision from then on. That's how you keep a file current. Any folders in the path are created for you.
Small text documents
Text up to 256 KiB goes in a single request, with no parts and no SHA-256.
curl -X PUT https://byterover.ai/api/v1/drives/drv_4h7k2m9q8r1t5v6w3x0y/documents/by-path \
-H "Authorization: Bearer $BYTEROVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"path": "/decisions/move-billing-queue.md",
"text": "# Move billing off the old queue\n\n## Context\n\nRetries on the old queue could charge a customer twice.",
"tags": ["payments"]
}'You get the same response as a completed upload: the document and an operation.
Tags
Add tags when you start an upload or send a small text document, and you can then limit searches to those files. Tags are a list of plain labels, such as ["legal", "2026"]. A document can have up to 32 tags, each 1 to 64 characters, with no = or control characters and no tag listed twice. See tags.
When you start an upload, you can also send content_type, the file's media type, such as application/pdf. It's optional and up to 255 characters.
If it fails
| You get | Cause | What to do |
|---|---|---|
400 INVALID_REQUEST | The path doesn't end in a file type ByteRover reads, the size is over 1 GB, or sha256 isn't a lowercase hex digest. | Fix the request. |
400 INVALID_REQUEST on complete | The list doesn't have every part, the parts aren't in order from 1, or an ETag doesn't match what storage received. | Send every part once, in order, each with the ETag its PUT returned. Send a part again if its ETag doesn't match. |
402 BALANCE_EXHAUSTED | The file would take the team past its free storage, and the balance is empty. | A team owner or admin adds money. See Pricing and billing. |
403 FORBIDDEN | The key has the Search scope. | Use a key with the Read and write scope. |
404 NOT_FOUND | The drive doesn't exist, or the key can't reach it. | Check the drive ID, and check which drives the key reaches with GET /v1/key. |
409 UPLOAD_CLOSED | The upload is already finished, or more than 24 hours old. | Start a new upload. |
503 STORAGE_UNAVAILABLE | Part of ByteRover isn't available right now. | Wait briefly, then retry. |
503 QUEUE_UNAVAILABLE on complete | The file was saved, but its processing couldn't be queued, so its operation is failed. Completing again doesn't queue it. | Wait briefly, then start a new upload to the same path. |
The operation is failed | ByteRover couldn't read the file. error.message says why, and error.code names the reason, such as IMAGE_ONLY_PDF_UNSUPPORTED for a PDF that's only images. | Fix the file and upload it again to the same path. A PDF that's only images can't be read yet. |
For the full list of codes, see Errors and retries.