---
title: Attachments and Images
path: tutorials/attachments-and-images
status: published
---

# Attachments and Images

ScaiSend handles binary content in two ways: **attachments** (file downloads included with the message) and **images** (visual content rendered inside the email). This page covers both, including the image library that lets you upload once and reuse across templates.

## Attachments

Attachments are base64-encoded in the request body. Use for PDFs, CSVs, spreadsheets, or any file the recipient should download.

```bash
curl -X POST https://scaisend.scailabs.ai/v3/mail/send \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{"to": [{"email": "ada@example.com"}]}],
    "from": {"email": "hello@mail.example.com"},
    "subject": "Your invoice",
    "content": [{"type": "text/plain", "value": "See attached."}],
    "attachments": [
      {
        "content": "JVBERi0xLjQK...",
        "filename": "invoice-042.pdf",
        "type": "application/pdf"
      }
    ]
  }'
```

```python
import base64
import os
import httpx

with open("invoice-042.pdf", "rb") as f:
    content_b64 = base64.b64encode(f.read()).decode()

resp = httpx.post(
    "https://scaisend.scailabs.ai/v3/mail/send",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_API_KEY']}"},
    json={
        "personalizations": [{"to": [{"email": "ada@example.com"}]}],
        "from": {"email": "hello@mail.example.com"},
        "subject": "Your invoice",
        "content": [{"type": "text/plain", "value": "See attached."}],
        "attachments": [
            {
                "content": content_b64,
                "filename": "invoice-042.pdf",
                "type": "application/pdf",
            }
        ],
    },
)
```

```typescript
import { readFile } from "node:fs/promises";

const pdf = await readFile("invoice-042.pdf");
await fetch("https://scaisend.scailabs.ai/v3/mail/send", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAISEND_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    personalizations: [{ to: [{ email: "ada@example.com" }] }],
    from: { email: "hello@mail.example.com" },
    subject: "Your invoice",
    content: [{ type: "text/plain", value: "See attached." }],
    attachments: [
      {
        content: pdf.toString("base64"),
        filename: "invoice-042.pdf",
        type: "application/pdf",
      },
    ],
  }),
});
```

### Attachment fields

| Field | Type | Required | Notes |
|-------|------|---------|-------|
| `content` | string (base64) | Yes | Binary file content, base64-encoded |
| `filename` | string | Yes | 1–255 chars; what the recipient sees in their mail client |
| `type` | string | No | MIME type; defaults to `application/octet-stream` |
| `disposition` | string | No | `attachment` (default) or `inline` |
| `content_id` | string | Conditional | Required for `disposition: inline`; the "CID" used to reference from HTML |

### Limits

- Max **10 attachments** per message.
- Total serialized body (including base64) must be **≤ 20 MB**. Base64 inflates binary by ~33%, so a 15 MB PDF plus a modest HTML body is fine; a 20 MB binary won't fit.
- For very large files (> 15 MB binary), host them externally and link from the email body.

## Inline attachments for embedded images

A classic use case: embed a company logo in the HTML so the email displays correctly even when images are blocked by default.

```json
{
  "personalizations": [{"to": [{"email": "ada@example.com"}]}],
  "from": {"email": "hello@mail.example.com"},
  "subject": "Welcome",
  "content": [
    {
      "type": "text/html",
      "value": "<img src=\"cid:logo\" /><h1>Welcome</h1>"
    }
  ],
  "attachments": [
    {
      "content": "iVBORw0KGgo...",
      "filename": "logo.png",
      "type": "image/png",
      "disposition": "inline",
      "content_id": "logo"
    }
  ]
}
```

The HTML references `cid:logo`; the attachment's `content_id` is `logo`. Mail clients resolve the CID and render the embedded image without fetching anything from the network.

This works but makes every message heavier. For images you reuse across many sends, use the image library.

## The image library

Upload an image once; reference it from any template or ad-hoc send. ScaiSend handles the serving, and — with the proxy embed mode — uses image loads as a backup open signal.

### Upload

```bash
curl -X POST https://scaisend.scailabs.ai/v3/images \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -F "file=@logo.png" \
  -F "alt_text=Acme logo" \
  -F "folder=brand"
```

```python
import os
import httpx

with open("logo.png", "rb") as f:
    resp = httpx.post(
        "https://scaisend.scailabs.ai/v3/images",
        headers={"Authorization": f"Bearer {os.environ['SCAISEND_API_KEY']}"},
        files={"file": ("logo.png", f, "image/png")},
        data={"alt_text": "Acme logo", "folder": "brand"},
    )
print(resp.json()["url"])
```

```typescript
const form = new FormData();
form.append("file", new Blob([await readFile("logo.png")], { type: "image/png" }), "logo.png");
form.append("alt_text", "Acme logo");
form.append("folder", "brand");

const resp = await fetch("https://scaisend.scailabs.ai/v3/images", {
  method: "POST",
  headers: { "Authorization": `Bearer ${process.env.SCAISEND_API_KEY}` },
  body: form,
});
const image = await resp.json();
console.log(image.url);
```

Response:

```json
{
  "id": "img_01HXYZ",
  "filename": "logo.png",
  "original_filename": "logo.png",
  "content_type": "image/png",
  "size": 42195,
  "width": 600,
  "height": 200,
  "url": "https://scaisend.scailabs.ai/i/img_01HXYZ",
  "thumbnail_url": "https://scaisend.scailabs.ai/i/img_01HXYZ?variant=thumbnail",
  "alt_text": "Acme logo",
  "folder": "brand",
  "created_at": "2026-04-23T10:00:00Z",
  "updated_at": "2026-04-23T10:00:00Z"
}
```

- Max file size: **10 MB**.
- Supported formats: **JPEG, PNG, GIF, WebP**.
- `folder` is optional; use it to organize assets ("brand", "marketing-q2", etc.).

### Reference in HTML

Use the returned `url` as any ordinary `<img src="...">`:

```html
<img src="https://scaisend.scailabs.ai/i/img_01HXYZ" alt="Acme logo" />
```

### Two embedding modes

The tenant's `image_embed_mode` setting controls how ScaiSend treats `/i/...` URLs when the email goes out:

| Mode | Behavior |
|------|----------|
| `proxy` (default) | The URL stays as-is in the HTML. The recipient's client fetches it on demand. Each fetch is recorded as a possible-open signal. Smaller emails; requires the recipient to have internet access when viewing. |
| `cid` | ScaiSend downloads the image server-side, converts it to a `Content-ID` inline attachment, and rewrites the `<img src>` to `cid:...`. Bigger emails; works offline; no image-load tracking. |

Set per tenant:

```bash
curl -X PUT https://scaisend.scailabs.ai/api/admin/tenants/tnt_acme/tracking \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"image_embed_mode": "proxy"}'
```

See [Tracking](../concepts/tracking#image-embedding-modes) for the full discussion.

### Listing, updating, deleting

```bash
# List (supports folder, search, pagination)
curl "https://scaisend.scailabs.ai/v3/images?folder=brand&page=1&page_size=50" \
  -H "Authorization: Bearer $SCAISEND_API_KEY"

# List folder names only
curl https://scaisend.scailabs.ai/v3/images/folders \
  -H "Authorization: Bearer $SCAISEND_API_KEY"

# Update alt text or folder
curl -X PATCH https://scaisend.scailabs.ai/v3/images/img_01HXYZ \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"alt_text": "Acme corporate logo", "folder": "brand-2026"}'

# Delete
curl -X DELETE https://scaisend.scailabs.ai/v3/images/img_01HXYZ \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

Deleting an image doesn't affect emails already sent — those messages have either embedded the image as CID (if you were on `cid` mode) or stored the URL (which continues to resolve at `/i/img_01HXYZ?tombstone=true`, returning a 410 Gone for future requests).

## The public image proxy

`GET /i/{image_id}` serves images without authentication. It's designed to be the `src` value in email HTML.

Three reasons to use the proxy rather than linking to S3 or a CDN directly:

1. **Your backend storage URL is hidden.** Recipients see only `scaisend.scailabs.ai/i/...`. You can migrate the underlying store without changing any email HTML.
2. **Image loads are logged.** If a recipient opens an email with images enabled, the proxy records a possible-open event. It's a backup signal when the tracking pixel is blocked.
3. **Caching headers are set correctly.** ScaiSend sends `Cache-Control: public, max-age=31536000, immutable`, which keeps image content in downstream CDNs without repeatedly hitting origin.

HEAD requests work — useful for preflight or link-checking tooling.

## What about raw URLs to your own server?

You can, of course, just point `<img src>` at an image on your own server or CDN. ScaiSend doesn't require you to use its image library. But two things to keep in mind:

- **You lose the backup-open signal.** Only tracked resources (the pixel and `/i/` proxy) count as opens.
- **You own the hotlink problem.** A popular email means millions of hits against your origin. Make sure it's cached.

For one-off images (a personalized chart, a QR code), hosting on your own infrastructure is fine. For recurring brand assets, use the image library.

## What's next

- [Sending Mail](sending-mail) — the top-level request with attachments.
- [Templates](templates) — referencing images in template HTML.
- [Tracking](../concepts/tracking) — how image embedding modes affect tracking.
