> 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.

# Uploading an Attachment to Benchling

## Overview

Attachments are arbitrary files that can be uploaded to Benchling and added to schema fields and some other objects in Benchling. The [V3 API](/docs/v3-api-overview) calls them attachments; the V2 API called the same model a blob.

After an attachment has been uploaded, it can be linked to another object, such as a Result or an entry, through an `Attachment` schema field. Attachments are not accessible on their own, and can only be accessed from a parent object they are linked to.

In V3 an upload takes two objects. You upload the bytes to a **scratch file**, then create an **attachment** from that scratch file. To put a file into the body of a notebook entry, you skip the attachment and insert the scratch file into the entry as an attachment part, as shown in [Attaching a file to a notebook entry](#attaching-a-file-to-a-notebook-entry).

> **Info**
>
> The V3 scratch file, attachment, and entry endpoints are beta. Set `early_access="true"` on the client, as in the examples below, or send the `EARLY-ACCESS: true` header on raw HTTP requests. See the [V3 Stability Guidelines](/docs/stability-guidelines).

Every example below uses this client. Pass your tenant's root URL; the SDK adds `/api/v3` itself. See [Getting started with the V3 SDK](/docs/getting-started-with-the-v3-sdk) to install it.

```python
from benchling_v3_sdk import Benchling

benchling = Benchling(
    base_url="https://yourcompany.benchling.com",
    api_key="<your-api-key>",
    early_access="true",
)
```

## Uploading a file

Uploading a file takes four calls:

1. Create a scratch file with `scratch_file.create`. The response carries an `upload_url`.
2. `PUT` the raw file bytes to that `upload_url`, with the header `x-amz-server-side-encryption: AES256`. The `Content-Type` header you send on this `PUT` becomes the attachment's MIME type, so set it to the file's type.
3. Mark the upload finished by setting the scratch file's `upload_status` to `SUCCEEDED` with `scratch_file.update`.
4. Create the attachment from the scratch file with `attachment.create`.

There is no base64 encoding and no MD5 hash in V3; the bytes go to the upload URL as they are. Setting `generate_image_preview=True` lets Benchling show a preview of an image, which is what the V2 `VISUALIZATION` blob type did. There is also a `generate_pdf_preview` flag for files that should get a PDF preview.

```python
import httpx

with open("hello.png", "rb") as f:
    contents = f.read()

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

upload = httpx.put(
    scratch_file.upload_url,
    content=contents,
    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,
)
print(attachment.id)
```

For UTF-8 text up to 10 MiB, such as a CSV, `scratch_file.create_and_upload(contents=..., filename=...)` replaces the first three calls with one. Binary files such as images go through the upload URL.

> **Warning**
>
> **Store your attachment ID**
>
> Attachments are not searchable on their own: V3 can get an attachment by ID, but it has no endpoint that lists them. If you do not persist the ID before linking the attachment to a parent object, it will be "lost" and you'll have to re-upload the file. Make sure to store the returned attachment ID so you can link it to a parent object.

## Uploading a large file

V2 split files larger than 10MB into parts with its multipart blob endpoints. V3 has no multipart upload: the whole file goes to the scratch file's upload URL in one `PUT`. See [Large Attachment Upload](/docs/large-attachment-upload) for streaming a large file from disk.

## Linking attachments to schema fields

Most types of [schemas](/docs/schemas) within Benchling support a schema field type called `Attachment`, called `blob_link` in V2. These fields are where attachments are linked to objects and become accessible in Benchling.

Once an attachment has been created, update the associated object and set the attachment field to the attachment ID, passed as a plain string. V3 attachment IDs look like `attachment_` followed by letters and digits.

An `Attachment` field also accepts a V2 blob ID (a UUID) when you write it, so files uploaded through the V2 API can be linked the same way. Reads always return the V3 attachment ID, even for a field written with a blob ID.

### Upload the file

For this example, we'll upload `hello.png` as shown in [Uploading a file](#uploading-a-file) and keep the attachment ID it returns.

### Create an Empty Attachment field

As an example, we're going to use a notebook entry schema with a single `Attachment` field. See our [help documentation](https://help.benchling.com/hc/en-us/articles/9684227216781-Select-an-Entry-Schema-for-an-Entry-Enterprise-) for more information on entry schemas. This is just for example purposes though, and the process is nearly identical for all other schematized objects in Benchling.

![An entry schema definition with a single field named File, of type Attachment](/_fern-img/1130a9e1d242cebe607a3125e770920b0e80f77a5a2dbdc7fbc81ea829155e98.webp)

We'll now create an entry and assign it the example schema.

![A Benchling notebook entry named Blob Example, with an empty Attachment field named File in its metadata](/_fern-img/aac5e68c6ee8e6424333b3d06140b62d2482a7108f4811eeb0fb046b0c53f16b.webp)

We now retrieve the API ID of that entry by either:

1. [Copying the API ID in the UI](/docs/getting-started-1#copy-api-ids)
2. Listing entries and filtering by name:

```python
entry = next(iter(benchling.entry.list(name_any_of_case_sensitive="Blob Example")))
print(entry.id)
```

We now have our entry's API ID! For this example, it's `etr_ZbYBCbAi`.

Now that we have the API ID of the destination parent resource and the ID of the attachment we uploaded, we can finally link the two together.

### Link the attachment

To link the attachment to the entry, update that field on the entry with `entry.update`. Schema fields are keyed by the field's `name`, not its display name; this example assumes the "File" field's name is `file`:

```python
from benchling_v3_sdk import FieldValueInput

updated_entry = benchling.entry.update(
    "etr_ZbYBCbAi",
    schema_fields={"file": FieldValueInput(value=attachment.id)},
)
```

The SDK sends this request body to `PATCH /api/v3/entry/etr_ZbYBCbAi`, and the response is the updated entry:

```json
{
  "schemaFields": {
    "file": {"value": "<attachment-id>"}
  }
}
```

And we can see our file in the UI!

![The Benchling entry metadata panel showing hello.png attached to the File field](/_fern-img/de59d0fbd26228f2b8cf4ad55d25cc75d8605c2692ca7f6fbad809a826e3844d.webp)

## Attaching a file to a notebook entry

Attachments in the body of a notebook entry are separate from `Attachment` schema fields. To add a file to the entry itself, upload it to a scratch file (steps 1 to 3 of [Uploading a file](#uploading-a-file)) and insert it into the entry as an `attachment` part with `entry.update`. The part takes the scratch file ID, and the scratch file upload must be complete first.

An insert names the entry's current version, which you read from the entry. If the entry changed since you read it, the request fails with HTTP 409, so read the version again and retry.

```python
entry = benchling.entry.get("etr_ZbYBCbAi")

benchling.entry.update(
    "etr_ZbYBCbAi",
    parts_mutation={
        "operation_type": "insert",
        "version": entry.version_metadata.version_id,
        "coordinates": {"position": "end_of_document"},
        "parts": [{"content_type": "attachment", "scratch_file_id": scratch_file.id}],
    },
)
```

`position` can also be `beginning_of_document`, or a document part ID to insert immediately before that part. `entry.create` accepts the same attachment part in its `parts` list, for a new entry that starts with the file.

The `parts_mutation` is passed as a plain dict because the SDK's `UpdateDocumentPartsInput` model types `coordinates` for the delete operation only, so it rejects the `position` an insert needs.

Check out the other examples for more info on how to use the developer platform, or try this out with new schema types like entities or results!