A 201 from confirm means the document row exists and processing was queued. It does not mean the file is indexed.

Add Data screen for bringing files into a workspace
Add Data is the screen for files. The HTTP contract below is the presigned upload, not a second product.

Upload files

Put one fictional text file into a stored collection and read its status back. Hands-on time is about 20 minutes. Queue time after confirm is extra and can be minutes.

Purpose

You can repeat the upload with urllib and explain each status. Level: someone who can call HTTP. Role: document:upload. Mode: stored. Reuse the workspace from Where to work. Provider keys are not required for the upload itself.

Sample

Create harbor-note.txt with exactly this line: Harbor Sample delivery window is 14 days. Format is txt. Do not use a real contract or a downloaded corpus. Delete the local file when you finish. The collection row remains until someone with collection management deletes it.

Steps

  1. POST /api/collections/{collectionId}/documents/upload-url with the JSON below. Expect documentId, storageKey, uploadUrl, expiresIn, and headers.
  2. PUT the file bytes to uploadUrl. Send only the Content-Type from headers. Do not send the Bundata session or bearer token to that URL. Do not print uploadUrl.
  3. POST /api/collections/{collectionId}/documents/confirm with the same documentId, storageKey, name, format, and byte size. A new row returns 201 and status processing.
  4. GET /api/collections/{collectionId}/documents and find that documentId. Stay on processing until it moves. There is no separate status URL.

Fields

FieldTypeRule
filenamestring1–500 characters. Used to build the storage key.
formatenumpdf, docx, txt, image, csv, xlsx.
sizeBytesintegerOptional on upload-url. Required on confirm. 0 through 500 MiB. Positive on upload-url when sent.
contentTypestringOptional, max 200. The response headers are what the PUT must send.
relativePathstringOptional, max 2048. A folder-relative path for discovery. Not a metadata editor.
documentIduuidReturned by upload-url. You do not invent it.
storageKeystringReturned by upload-url. Must still match the collection on confirm.

Example

Non-browser clients use a bearer token from POST /auth/login. Read it from the environment. Never print it. Browser pages use the session cookie instead, and must not send both.

curl -sS -X POST "$BUNDATA_ORIGIN/api/collections/$COLLECTION_ID/documents/upload-url" \
  -H "Authorization: Bearer $BUNDATA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filename":"harbor-note.txt","format":"txt","sizeBytes":42,"contentType":"text/plain"}'
import json, os, urllib.request
origin = os.environ["BUNDATA_ORIGIN"].rstrip("/")
token = os.environ["BUNDATA_TOKEN"]
collection = os.environ["BUNDATA_COLLECTION_ID"]
body = json.dumps({"filename": "harbor-note.txt", "format": "txt", "sizeBytes": 42, "contentType": "text/plain"}).encode()
req = urllib.request.Request(origin + "/api/collections/" + collection + "/documents/upload-url", data=body, method="POST")
req.add_header("Authorization", "Bearer " + token)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as res:
    issued = json.load(res)
payload = b"Harbor Sample delivery window is 14 days.\n"
put = urllib.request.Request(issued["uploadUrl"], data=payload, method="PUT")
put.add_header("Content-Type", issued["headers"]["Content-Type"])
urllib.request.urlopen(put).close()
confirm = json.dumps({
    "documentId": issued["documentId"], "storageKey": issued["storageKey"],
    "name": "harbor-note.txt", "format": "txt", "sizeBytes": len(payload),
}).encode()
creq = urllib.request.Request(origin + "/api/collections/" + collection + "/documents/confirm", data=confirm, method="POST")
creq.add_header("Authorization", "Bearer " + token)
creq.add_header("Content-Type", "application/json")
with urllib.request.urlopen(creq) as res:
    row = json.load(res)
assert res.status == 201
assert row["status"] == "processing"

The standard library is the only dependency. BUNDATA_TOKEN, BUNDATA_ORIGIN, and BUNDATA_COLLECTION_ID are placeholders you set in the environment.

Recovery

ResultWhat to do
201 and processingPoll GET /api/collections/{id}/documents. Do not upload a second copy.
Same confirm again, identical fields200 and the existing row. Safe retry.
Same documentId with a different name, size, or key409. Reconcile. Do not mint a new id to hide the conflict.
S3_NOT_CONFIGURED or S3_PRESIGN_UNAVAILABLEUpload is not available in that deployment. Stop.
DOCUMENT_PROCESSING_QUEUE_UNAVAILABLEThe row may exist with error. Retry the same confirm.
403The role lacks document:upload.
upload-directA gated fallback. When the gate is off the route is not found. It is not the production contract.

Errors use { "error": { "code", "message", "statusCode" } }. A signed URL expires. Request a new upload-url rather than retrying a dead PUT.

Limits

Images can be stored and are not discovery-eligible. There is no .meta.json sidecar import and no API that writes arbitrary file metadata. relativePath is a path, not a tag.

Check

Success: the document list shows harbor-note.txt moving from processing toward ready when text is available. Failure: a viewer token receives 403 and the list is unchanged.

Related documentation