---
title: "POST /v1/jobs/{jobId}/readouts/upload"
description: Upload a readout file to a job
full: true
---

**Auth:** required. Send an `ivy_` API key (`Authorization: Bearer ivy_…` or `X-Ivy-Api-Key`), a Clerk session JWT (`Authorization: Bearer …`) or the Clerk `__session` cookie (browser sessions). See [Authentication](/docs/api/authentication).

Uploads one readout file (a CSV by default) to your file storage and records it against a job you can write to, as a `readouts_csv` output of that job. Send the file as `multipart/form-data`. The file field is named `file`; if it is missing, the first file in the form is used. The file can be up to 100 MB.

The optional `projectId` form field files the upload under that project. It must be a project you own, and with a project-scoped API key it must also be one of the key's projects; otherwise the request returns 404. The response carries the storage key, the size in bytes, the content type (`text/csv` when the upload does not name one) and the SHA-256 hash of the file.

A project viewer cannot upload; they get the same 404 as for a job that does not exist. With a project-scoped API key, a job outside the key's projects also returns 404. Only `POST` is served.

## Request

#### Path parameters

- `jobId` (string (uuid)) —
Job UUID.

## Responses

### `200` OK

The file was stored and recorded

Response shape:

- `success` (enum (true)) —

- `objectKey` (string) —
Storage key of the uploaded file

- `sizeBytes` (integer) —

- `contentType` (string) —
The type the upload named, or `text/csv`

- `hash` (string) —
SHA-256 of the file, in hex

### `400` Bad Request

The job id is empty, or the form has no file

Response shape: `object`

### `401` Unauthorized

Authentication required

Response shape: `object`

### `404` Not Found

The job does not exist, you cannot write to it, it is outside your API key's project scope, or `projectId` names a project you do not own or your key cannot reach. All of these return an error body with `NOT_FOUND`.

Response shape: `object`

### `405`

The method is not `POST`; the `Allow` header says `POST`

Response shape: `object`

### `429` Too Many Requests

Rate limit exceeded

Response shape: `object`

### `500` Internal Server Error

Storage is not configured, the form could not be read (for example the file is over 100 MB), or the upload or the record failed; `message` carries the cause

Response shape: `object`

**curl**

```bash
curl -X POST 'https://ivybiosciences.com/api/v1/jobs/{jobId}/readouts/upload' \
  -H "Authorization: Bearer $IVY_API_KEY"
```

**JavaScript**

```js
const res = await fetch('https://ivybiosciences.com/api/v1/jobs/{jobId}/readouts/upload', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.IVY_API_KEY}`,
  },
});
const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://ivybiosciences.com/api/v1/jobs/{jobId}/readouts/upload",
    headers={"Authorization": f"Bearer {os.environ['IVY_API_KEY']}"},
)
data = res.json()
```
