> ## Documentation Index
> Fetch the complete documentation index at: https://docs.my-aichatbot.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending files

> Attach images and documents by link, or upload them and use the file_id.

The `image`, `file`, and `card` blocks can get their content from two places.

| Source           | How                                  | Cost                                     | Control                                                     |
| ---------------- | ------------------------------------ | ---------------------------------------- | ----------------------------------------------------------- |
| **External URL** | Put any `https://` URL in `url`      | Free — not our bandwidth                 | None. A dead link shows a fallback.                         |
| **Upload**       | Upload first, then use the `file_id` | Counts toward your storage and bandwidth | Signed links, storage rules, deleted with the conversation. |

If the file already lives somewhere public, just link to it. There is nothing to upload:

```json theme={null}
{ "type": "image", "url": "https://example.com/chart.png", "alt": "June revenue" }
```

## Uploading

One multipart POST. Two endpoints, same behaviour. Pick the one that matches the key you already have.

<CodeGroup>
  ```bash Response URL theme={null}
  curl -X POST "$RESPONSE_URL/files" \
    -F "file=@june-report.pdf" \
    -F "filename=june-report.pdf"
  ```

  ```bash API key theme={null}
  curl -X POST "https://app.my-aichatbot.com/api/v1/bots/$CHATBOT_ID/files" \
    -H "Authorization: Bearer $MAC_KEY" \
    -F "file=@june-report.pdf"
  ```
</CodeGroup>

The `response_url` one is best mid-workflow: no API key needed, because the token already carries permission.

<ParamField body="file" type="binary" required>
  The file part. Missing it returns `400 MALFORMED_MULTIPART`.
</ParamField>

<ParamField body="filename" type="string">
  Replaces the file's own name. Cut to 255 characters.
</ParamField>

```json 201 Created theme={null}
{
  "ok": true,
  "file": {
    "file_id": "6f1c2a84",
    "name": "june-report.pdf",
    "size_bytes": 482113,
    "mime_type": "application/pdf",
    "expires_at": null
  }
}
```

## Then use it

```json theme={null}
{
  "messages": [
    {
      "blocks": [
        { "type": "text", "text": "Here is your report 📊" },
        { "type": "file", "file_id": "6f1c2a84", "name": "june-report.pdf" }
      ]
    }
  ]
}
```

Blocks only store the `file_id`. Each time someone reads the message, we make a fresh short-lived link. Links in your JSON never go stale.

## Uploading in two steps

In Make or n8n, this is two HTTP modules: upload, then map `file.file_id` into the message body.

<Steps>
  <Step title="Upload">
    POST the binary to `{{response_url}}/files` as `multipart/form-data`. Read back `file.file_id`.
  </Step>

  <Step title="Post the message">
    POST Block Kit to `{{response_url}}`, mapping the `file_id` from step 1.
  </Step>
</Steps>

## What we accept

<ParamField body="Images" type="10 MB max">
  PNG, JPEG, GIF, WebP, AVIF.
</ParamField>

<ParamField body="Other files" type="25 MB max">
  PDF, XLSX, DOCX, PPTX, ZIP, and anything else not blocked below.
</ParamField>

We check the file's real type by **reading its first bytes**. We never trust the name or the `Content-Type`. Renaming `evil.exe` to `report.pdf` does not work.

<Warning>
  **Images are converted to WebP when uploaded, and only that copy is kept.** The original is thrown away. The size limit is checked against what you uploaded, not the smaller converted file.
</Warning>

### Rejected

| Type                       | Status | Code                       |
| -------------------------- | ------ | -------------------------- |
| Programs (PE, ELF, Mach-O) | `422`  | `FILE_TYPE_BLOCKED`        |
| HTML                       | `422`  | `FILE_TYPE_BLOCKED`        |
| HEIC images                | `415`  | `IMAGE_FORMAT_UNSUPPORTED` |

HTML is blocked because rich HTML belongs in Pages, not chat attachments. HEIC is refused instead of stored broken — send JPEG, PNG, or WebP instead.

## Errors

| Status | Code                       | Meaning                                       |
| ------ | -------------------------- | --------------------------------------------- |
| `400`  | `MALFORMED_MULTIPART`      | Not `multipart/form-data`, or no `file` part. |
| `402`  | `STORAGE_CAP_REACHED`      | Workspace storage is full.                    |
| `403`  | `wrong_chatbot`            | The key belongs to a different bot.           |
| `413`  | `FILE_TOO_LARGE`           | Over 10 MB (image) or 25 MB (other).          |
| `415`  | `IMAGE_FORMAT_UNSUPPORTED` | HEIC.                                         |
| `422`  | `FILE_TYPE_BLOCKED`        | A program or HTML file.                       |
| `429`  | `rate_limited`             | Over 20 uploads per minute.                   |

<Note>
  Storage is reserved before we save the file. A `402` means nothing was saved and nothing was counted.
</Note>
