> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.labric.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.labric.co/_mcp/server.

# Upload files

Files reach Labric through a signed storage URL: you ask the API for an upload URL, PUT the bytes to that URL, then confirm the upload. Only the two small JSON requests pass through the API; the bytes go straight to storage. The SDK does all three steps in one call, and the REST API exposes each step so any HTTP client can run them.

Every upload names its source. Pass `job_execution_id` to store the file as an artifact of a job execution, which also records provenance linking the file to that run, or `instrument_id` to attach the file to an instrument the Sync app cannot reach so its triggers and parsers pick it up. At least one of the two is required.

## Prerequisites

* An API key with the **write** scope, created under **Settings > API Keys** on [platform.labric.co](https://platform.labric.co) and exported as `LABRIC_API_KEY`
* For the SDK, Python 3.9 or later with `pip install labric`

## Upload with the SDK

`client.files.upload()` takes an open file, bytes, or a `(filename, content)` tuple and returns the stored file's record once its bytes are confirmed:

```python
from labric import Labric

client = Labric()

with open("results.csv", "rb") as f:
    record = client.files.upload(file=f, job_execution_id=execution_id)

print(record.file_id, record.size_kilobytes)
```

The platform records a filename, so pass a tuple when the content has no name of its own. The content type is guessed from the filename unless you supply one:

```python
client.files.upload(
    file=("micrograph.tif", tiff_bytes, "image/tiff"),
    instrument_id="your-instrument-id",
)
```

Inside a Labric job, `job_execution_id` defaults to the `LABRIC_JOB_EXECUTION_ID` environment variable, so a script running in the sandbox can omit it. Open files are streamed in 1 MiB chunks, so a large file never sits in memory.

`client.files.create_upload_url()` and `client.files.confirm_upload()` are also available for scripts that want to drive the steps themselves, for example to hand the PUT to another process.

## Upload with the REST API

### 1. Create an upload URL

```bash
curl -X POST https://platform.labric.co/api/v1/files/upload-url \
  -H "Authorization: Bearer $LABRIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "results.csv", "content_type": "text/csv", "job_execution_id": "'"$EXECUTION_ID"'"}'
```

The response holds the file's ID, the signed URL, the headers the PUT must carry, and when the URL expires:

```json
{
  "file_id": "f3a9b2d7-8c4e-4a1f-9b2d-7e8c4e1f9b2d",
  "upload_url": "https://storage.googleapis.com/labric-files/f3a9b2d7-...?X-Goog-Algorithm=GOOG4-RSA-SHA256&...",
  "headers": {
    "Content-Type": "text/csv",
    "x-goog-if-generation-match": "0",
    "x-goog-content-length-range": "0,524288000"
  },
  "expires_at": "2026-09-21T15:32:10.481Z"
}
```

### 2. PUT the bytes to the URL

Send the file as the request body with exactly the returned headers and no `Authorization` header. The URL is signed for those headers, so changing or omitting one makes storage reject the request.

```bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: text/csv" \
  -H "x-goog-if-generation-match: 0" \
  -H "x-goog-content-length-range: 0,524288000" \
  --data-binary @results.csv
```

### 3. Confirm the upload

```bash
curl -X POST https://platform.labric.co/api/v1/files/$FILE_ID/confirm-upload \
  -H "Authorization: Bearer $LABRIC_API_KEY"
```

Confirming records the stored file's size and checksum, makes the file visible in listings, and notifies triggers and parsers. Until then the file does not appear anywhere. The response is the same record `client.files.upload()` returns.

## Limits and retries

* An upload URL is valid for 15 minutes. If it expires or the PUT fails, request a new URL. For an unconfirmed upload at the same instrument path, the API returns a fresh URL for the same file ID instead of a conflict.
* The URL only creates an object; a second PUT to a used URL is refused by storage with a 412.
* Storage refuses bodies over 500 MB before they are stored. Files that turn out to be native executables are discarded at confirmation with a 400.
* An instrument can hold one file per name, so a name already confirmed for that instrument is a 409. Job artifacts have no such constraint.

## Small files in one request

`POST /api/v1/files` accepts a single `multipart/form-data` request for files under 4.5 MB, which is the request body limit at the platform edge. It takes the same `job_execution_id` and `instrument_id` fields as form parts. Use it when a one-request upload is simpler than the signed flow and the file is small.

```bash
curl -X POST https://platform.labric.co/api/v1/files \
  -H "Authorization: Bearer $LABRIC_API_KEY" \
  -F "file=@notes.txt" \
  -F "instrument_id=$INSTRUMENT_ID"
```