> This page is for Developer Documentation.

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

# Image File Upload

> **Info**
>
> This page uses the beta V3 scratch file and attachment endpoints. Set `early_access="true"` on the
> client or send the `EARLY-ACCESS: true` header on raw HTTP requests. See the
> [V3 Stability Guidelines](/docs/stability-guidelines).

This example assumes `io` and `httpx` are imported, that `benchling` is a V3 SDK client (see
[Getting started with the V3 SDK](/docs/getting-started-with-the-v3-sdk)), and that
`image_loader` and `cell` come from an open XLSX worksheet, for instance
[`openpyxl`](https://openpyxl.readthedocs.io/)'s image loader and a cell you have already located.
`httpx` is installed with the V3 SDK.

#### Get the image file

Get the image in question and write it into an in-memory buffer as PNG bytes. This example
pulls an image out of a single cell in an XLSX worksheet.

**`Python (lines 1-5)`**

```python Python (lines 1-5)
byteImgIO = io.BytesIO()

image = image_loader.get(cell.coordinate)

image.save(byteImgIO, "PNG")
```

#### Create a scratch file

A V3 upload starts with a scratch file. The response carries the `upload_url` the bytes go to.

**`Python (line 7)`**

```python Python (line 7)
scratch_file = benchling.scratch_file.create(filename="plot1.png", name="plot1.png")
```

#### Upload the file data

`PUT` the raw PNG bytes to the upload URL with the `x-amz-server-side-encryption: AES256`
header. The `Content-Type` header on this `PUT` becomes the attachment's MIME type. There is no
base64 encoding and no MD5 hash, as there was with V2 blobs.

**`Python (lines 9-14)`**

```python Python (lines 9-14)
upload = httpx.put(
    scratch_file.upload_url,
    content=byteImgIO.getvalue(),
    headers={"x-amz-server-side-encryption": "AES256", "Content-Type": "image/png"},
)
upload.raise_for_status()
```

#### Mark the upload finished

Set the scratch file's `upload_status` to `SUCCEEDED` so Benchling knows the bytes are in place.

**`Python (line 16)`**

```python Python (line 16)
benchling.scratch_file.update(scratch_file.id, upload_status="SUCCEEDED")
```

#### Create the attachment

Create an attachment from the scratch file. `generate_image_preview=True` is what makes the
attachment render as an image preview in the Benchling UI, as the V2 `VISUALIZATION` blob
type did.

**`Python (lines 18-21)`**

```python Python (lines 18-21)
attachment = benchling.attachment.create(
    scratch_file_id=scratch_file.id,
    generate_image_preview=True,
)
```

#### Persist the attachment ID

Collect the attachment ID so you can reference it later. This example uses it in calls to
assay results, but persisting it matters for every use case: attachments are not searchable on
their own, so an unrecorded ID means re-uploading the file.

**`Python (line 23)`**

```python Python (line 23)
plot_id = attachment.id
```

#### Complete files

**`Python`**

```python title="Python"
byteImgIO = io.BytesIO()

image = image_loader.get(cell.coordinate)

image.save(byteImgIO, "PNG")

scratch_file = benchling.scratch_file.create(filename="plot1.png", name="plot1.png")

upload = httpx.put(
    scratch_file.upload_url,
    content=byteImgIO.getvalue(),
    headers={"x-amz-server-side-encryption": "AES256", "Content-Type": "image/png"},
)
upload.raise_for_status()

benchling.scratch_file.update(scratch_file.id, upload_status="SUCCEEDED")

attachment = benchling.attachment.create(
    scratch_file_id=scratch_file.id,
    generate_image_preview=True,
)

plot_id = attachment.id
```

To link the attachment to a schema field, or to put the image into the body of a notebook entry,
see [Uploading an Attachment to Benchling](/docs/uploading-an-attachment-to-benchling). For large files, see
[Large Attachment Upload](/docs/large-attachment-upload).