Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "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
1
2
3
4
5
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "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
1
<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
1
2
3
4
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 for the full discussion.

Listing, updating, deleting#

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 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 — the top-level request with attachments.
  • Templates — referencing images in template HTML.
  • Tracking — how image embedding modes affect tracking.
Updated 2026-05-17 01:33:26 View source (.md) rev 1