---
title: "Presign a Document File Upload"
method: POST
path: "/v1/graphs/{graph_id}/operations/create-document-upload"
tags: ["Content Operations"]
---

# Presign a Document File Upload

`POST /v1/graphs/{graph_id}/operations/create-document-upload`

## Request

- Base URL: `https://api.robosystems.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.robosystems.ai/v1/graphs/{graph_id}/operations/create-document-upload`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Start uploading a document file: a PDF (a bank statement kept as evidence for a recorded balance, an invoice) or a PNG or JPEG photo (a receipt). Returns an upload id and a presigned URL: PUT the file there with the same Content-Type (and Content-Length, if declared), then call `complete-document-upload`, which creates the document. Nothing is recorded until then, and an upload never completed expires. A stored file is not indexed for search, does not count toward the plan's document limit, and never changes once stored.

**Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict.

## Path parameters

- `graph_id` string, required

## Headers

- `Idempotency-Key` string, nullable

## Request body

- CreateDocumentUploadOp: Body for create-document-upload: where to upload a document's file.
  - `file_name` string, required: The file's name, ending in its type's extension (`.pdf`, `.png`, `.jpg` or `.jpeg`).
  - `content_type` 'application/pdf' | 'image/png' | 'image/jpeg': The file's media type.
  - `file_size_bytes` integer, nullable: The file's exact size in bytes, at most 25 MB. When given it is signed into the upload URL, so an upload of any other size fails. Completing the upload checks the size either way.

## Response `200`

Successful Response

- OperationEnvelope: Uniform response shape for every operation endpoint. Every dispatch carries an `op_<ULID>` operation_id, which is the bridge to the monitoring surface: pass it to `GET /v1/operations/{operation_id}/stream` (see `routers/operations.py`) to subscribe to SSE progress events. Sync commands complete in the envelope itself (`status: "completed"`, HTTP 200); async commands (`status: "pending"`, HTTP 202) hand off to a background worker and stream their tail through that SSE endpoint. Failed dispatches still mint an `operation_id` so the audit log and any partial SSE events stay correlatable. `TResult` parameterizes `result` so per-op response shapes surface in OpenAPI. Operations that pin `OperationSpec.result_type` get `OperationEnvelope[YourEnvelope]` as their response model; the rest keep the default `Any` shape (`result: any | null` on the wire).
  - `operation` string, required: Kebab-case operation name
  - `operationId` string, required: op_-prefixed ULID for audit and SSE correlation
  - `status` 'completed' | 'pending' | 'failed', required: Operation lifecycle state
  - `result` unknown
  - `at` string, required: ISO-8601 UTC timestamp
  - `createdBy` string, nullable: User ID that initiated the operation
  - `idempotentReplay` boolean: True when this envelope came from the idempotency cache — the underlying command did not execute again. False on fresh executions.

## Other responses

- `400`: Invalid request
- `401`: Authentication required
- `403`: Access denied
- `404`: Resource not found
- `409`: Idempotency-Key conflict — key reused with different body
- `422`: Validation error
- `429`: Rate limit exceeded
- `500`: Internal server error

---

[API](https://skmtc.dev/robosystems/apis/robosystems-api.md) · [All operations](https://skmtc.dev/robosystems/apis/robosystems-api/llms.txt) · [OpenAPI document](https://skmtc.dev/robosystems/apis/robosystems-api/revisions/e661e2d5ca6a?raw)
