---
title: File uploads
description: Upload files once, let a run use them, and the agent attaches the right one to each upload field.
---

Forms often ask for a file: a resume, an ID scan, an invoice. Upload your files to Lovely Router once. When a run may use them, the agent attaches the file that fits each upload field, your CV for a "Resume" field and not your cover letter, even when both are PDFs.

## Upload a file

Send the raw bytes to `PUT /v1/browser/files/{name}`, with the file's type as `Content-Type`. An optional `description` query parameter helps the agent choose.

```bash title="cURL"
curl -X PUT "https://lovelyrouter.com/v1/browser/files/Maria_Garcia_CV_2026.pdf?description=Resume%2C%20updated%202026" \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/pdf" \
  --data-binary @Maria_Garcia_CV_2026.pdf
```

```python title="Python"
from pathlib import Path

path = Path("Maria_Garcia_CV_2026.pdf")
f = requests.put(
    f"{API}/files/{path.name}",
    headers={**H, "content-type": "application/pdf"},
    params={"description": "Resume, updated 2026"},
    data=path.read_bytes(),
).json()
print(f["id"], f["bytes"])
```

```js title="Node.js"
import { readFile } from 'node:fs/promises';

const name = 'Maria_Garcia_CV_2026.pdf';
const file = await fetch(`${API}/files/${encodeURIComponent(name)}?description=${encodeURIComponent('Resume, updated 2026')}`, {
  method: 'PUT',
  headers: { Authorization: headers.Authorization, 'content-type': 'application/pdf' },
  body: await readFile(name),
}).then((r) => r.json());
console.log(file.id, file.bytes);
```

Or send a multipart form to `POST /v1/browser/files`, with the file in a `file` field. `name` (defaults to the uploaded file's name) and `description` are optional fields:

```bash title="cURL"
curl https://lovelyrouter.com/v1/browser/files \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F "file=@Maria_Garcia_CV_2026.pdf;type=application/pdf" \
  -F "description=Resume, updated 2026"
```

Both return the stored file:

```json
{
  "id": "file_7a3c9e1b5d2f8a4c6e0b",
  "name": "Maria_Garcia_CV_2026.pdf",
  "mime": "application/pdf",
  "bytes": 84213,
  "sha256": "5f2b8c…",
  "description": "Resume, updated 2026",
  "created_at": "2026-10-11T09:20:44.000Z",
  "last_used_at": null
}
```

Without a `Content-Type`, the file is stored as `application/octet-stream`; send the real type so the agent and the website see it. Uploading again under the same name replaces the file and keeps its `id`. Files are encrypted at rest, like [vault secrets](/docs/browser-agent/logins#how-your-secrets-are-protected). The Python and Node.js samples use the `API`, `H` and `headers` setup from [Run your first task](/docs/browser-agent#run-your-first-task).

| Limit | Value |
| --- | --- |
| File size | 10 MB |
| Files per account | 100 |
| Name | 1 to 128 characters: letters, digits, spaces and `. _ - ( ) + ,`, starting with a letter, digit or `_` |
| Description | 300 characters (longer text is cut) |

## Let a run use your files

Pass `files` when you start a run: a list of file names or ids, or `"*"` for all your files.

```bash title="cURL"
curl https://lovelyrouter.com/v1/browser/runs \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "goal": "Apply for the Backend Engineer role as Maria Garcia (maria.garcia@example.com), based in Portland, Oregon. Attach her resume. Submit the application and tell me the application ID.",
    "start_url": "https://jobs.example.com/backend-engineer/apply",
    "files": ["Maria_Garcia_CV_2026.pdf", "Maria_Garcia_Cover_Letter.pdf"],
    "max_steps": 40
  }'
```

The agent can attach files only in runs that pass `files`, and only the files listed. An unknown name returns `400` with `Unknown file(s): CV.pdf. Upload with PUT /v1/browser/files/{name}.`

In the trace, an upload step's `action` starts with `upload:` and its `description` names the field. If the run pauses for your approval before submitting, `needs.fields` shows which file is attached, for example `Resume: file "Maria_Garcia_CV_2026.pdf"`.

## How the agent picks a file

At each upload field, the agent compares your files with what the field asks for. It looks at:

- each file's name, type, size and description
- the field's label and the file types it accepts
- your goal and the page around the field

It matches the document's purpose, not only its type. To help it:

- **Use descriptive names.** `Maria_Garcia_CV_2026.pdf` beats `scan_0412.pdf`.
- **Add a description** when the name can't say it all: "Resume, updated 2026", "Passport photo page".
- **Say in the goal which file to use** when it matters: "Attach her resume (not the cover letter)".
- **Pass only the files the run needs** rather than `"*"` when you have many similar files.

## List and delete files

```bash title="cURL"
# List your files, sorted by name
curl https://lovelyrouter.com/v1/browser/files -H "Authorization: Bearer $LOVELY_API_KEY"

# Delete by name or id
curl -X DELETE "https://lovelyrouter.com/v1/browser/files/Maria_Garcia_CV_2026.pdf" -H "Authorization: Bearer $LOVELY_API_KEY"
```

The list returns `{"object": "list", "data": [...]}` with one file object per file. `last_used_at` shows when a run last attached it. Deleting returns `{"deleted": "<name or id>"}`, or `404` with `No such file`. You can also upload and delete files in the [console](/app/browser) under **Secrets & files**.

## Errors

| Status | Message | Cause |
| --- | --- | --- |
| `400` | `File names are 1-128 chars: letters, digits, space . _ - ( ) + ,` | The name has other characters or is too long. |
| `400` | `Empty file` | The body has no bytes. |
| `400` | `Send multipart/form-data with a "file" field (and optional "description", "name")` | The multipart form has no `file` field. |
| `409` | `file limit reached (100); delete some first` | You already have 100 files. Replacing an existing name still works. |
| `413` | `Files are limited to 10 MB` | The file is larger than 10 MB. |
