# Item files

An item in a collection (see [Collections and items](collections-and-items.html)) can carry attached files: a customer's proposal, an order's invoice, a product photo. With `met.files` you attach them, list them, download them, rename them and delete them.

Reading requires `items:read`; attaching, renaming and deleting, `items:write`. Both work with a test key. Routes are flat (`/items/{itemId}/files`, `/item-files/{fileId}`): they operate on the key's workspace, and an item or a file from another workspace responds `404`.

## Attaching a file

From the bytes you already have (multipart, field `file`):

```ts
import { readFile } from 'node:fs/promises';

const bytes = await readFile('proposal.pdf');
const file = await met.files.upload(41207, bytes, {
  filename: 'Propuesta comercial (v2).pdf',
  contentType: 'application/pdf',
});
console.log(file.id, file.download_url);
```
```python
with open("proposal.pdf", "rb") as f:
    file = met.files.upload(41207, f.read(), filename="Propuesta comercial (v2).pdf", content_type="application/pdf")
print(file["id"], file["download_url"])
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/items/41207/files \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -F "file=@proposal.pdf;type=application/pdf"
```

Or from a public URL, without downloading and re-uploading the file on your server: Met downloads it for you.

```ts
await met.files.attachUrl(41207, 'https://mystore.co/invoices/10293.pdf', { filename: 'Factura 10293.pdf' });
```
```python
met.files.attach_url(41207, "https://mystore.co/invoices/10293.pdf", filename="Factura 10293.pdf")
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/items/41207/files \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://mystore.co/invoices/10293.pdf","filename":"Factura 10293.pdf"}'
```

- The cap is **25 MB** per file, through both paths.
- The URL has to be public: Met does not download internal or private addresses (SSRF protection). It has 20 seconds to respond; if it responds with an error or returns no content, the attachment is not created. The file's type comes from its `Content-Type`.
- Without `filename`, the name comes from the URL.
- The file also shows up in the workspace's Media Library (`asset_id`), and Met extracts its text in the background so a Met can read it in a conversation.

## What it returns

```json
{
  "id": 8123,
  "item_id": 41207,
  "original_name": "Propuesta comercial (v2).pdf",
  "filename": "1753848291233-propuesta-comercial.pdf",
  "mime_type": "application/pdf",
  "size": 184320,
  "storage_path": "items/41207/1753848291233-propuesta-comercial.pdf",
  "created_by": 42,
  "created_at": "2026-07-30T04:44:51.233Z",
  "asset_id": null,
  "download_url": "https://…/object/sign/…?token=…"
}
```

| Field | What it is |
|---|---|
| `original_name` | The name people see and the one it downloads with. |
| `filename` | The sanitized name it was stored with. Do not show it. |
| `storage_path` | **What you should store** if you keep a reference: it is the only stable thing. |
| `download_url` | Only when attaching: a **signed link that expires after 7 days**. It is for using right away, not for storing. |

Files live in private storage: there is no public URL that opens without credentials.

## Listing and downloading

```ts
import { writeFile } from 'node:fs/promises';

const files = await met.files.list(41207); // newest to oldest

for await (const f of met.files.iterate(41207)) {
  console.log(f.id, f.original_name, f.size);
}

const { data, contentType, filename } = await met.files.download(8123);
await writeFile(filename ?? 'file', data);
```
```python
files = met.files.list(41207)
for f in met.files.iterate(41207):
    print(f["id"], f["original_name"], f["size"])

download = met.files.download(8123)
with open(download["filename"] or "file", "wb") as out:
    out.write(download["data"])
```
```bash
curl -L https://api.met.meteor.com.co/api/v1/item-files/8123/download \
  -H "Authorization: Bearer $METEOR_API_KEY" -o proposal.pdf
```

- `list()` without parameters returns the latest 100. `iterate()` walks **all** of them, page by page, with a cursor.
- `download()` returns **bytes, not JSON**, with the file's type and name. Unlike `download_url`, this route does not expire: it validates the key on every call.

## Renaming and deleting

```ts
await met.files.update(8123, { name: 'Propuesta aprobada.pdf' });
await met.files.delete(8123); // { message: 'File deleted' }
```
```python
met.files.update(8123, name="Propuesta aprobada.pdf")
met.files.delete(8123)
```

Renaming only changes `original_name`: the content and `storage_path` are untouched. Deleting removes the file and its record; the item still exists.

## From an assistant (MCP)

The MCP server lists an item's files, but does not upload or download bytes: a file does not travel through an assistant's conversation. For that, use the SDK or the API.

## Other workspace files

The **Media Library** —the workspace's images, videos and documents, organized in folders— is a different resource: `met.assets`, with the `files:read` and `files:write` scopes.

## Common errors

| Code | What happened |
|---|---|
| `400` `validation_error` | You sent neither `file` nor `url`; the URL is not allowed, responded with an error or returned no content; or the URL's file exceeds 25 MB. |
| `413` `FILE_TOO_LARGE` | The file uploaded via multipart exceeds 25 MB. |
| `404` `not_found` | The item or the file does not exist in this workspace. |
| `403` `missing_scope` | The key does not carry `items:write` (or `items:read` for reading). |
