Skip to content

Attachments

Visitors can attach images (JPEG, PNG, GIF, WebP) and PDFs to a message by clicking the paperclip, dragging files onto the composer, or pasting from the clipboard. The worker forwards them to Claude as native image / document content blocks, so the model can read a screenshot of an error, a receipt, or a multi-page PDF.

Attachments are off in the widget by default and on in the worker by default (passthrough mode). Enable the widget side to start using them.

<ChatWidget apiUrl="https://your-worker.workers.dev" attachments />
<script>
window.ClaudiusConfig = {
apiUrl: "https://your-worker.workers.dev",
attachments: true,
};
</script>
<claudius-chat api-url="https://your-worker.workers.dev" attachments></claudius-chat>

Pass an object instead of true to tune the client-side limits:

OptionDefaultDescription
maxSizeBytes5242880 (5 MB)Largest file the composer accepts
maxCount5Files per message
allowedTypesJPEG, PNG, GIF, WebP, PDFAccepted MIME types
<ChatWidget
apiUrl=""
attachments={{ maxSizeBytes: 2 * 1024 * 1024, maxCount: 2, allowedTypes: ["image/png", "image/jpeg"] }}
/>

The widget validates type, size, and count before anything is uploaded and shows an inline message for rejected files. Keep these limits at or below the worker’s so a file the composer accepts is never refused later.

VariableDefaultDescription
ATTACHMENTS_ENABLEDtrueSet to false to reject any request carrying attachments (400 ATTACHMENTS_DISABLED)
ATTACHMENT_TYPESimage/jpeg,image/png,image/gif,image/webp,application/pdfComma-separated allowlist. Only these five are forwarded natively; others are refused
ATTACHMENT_MAX_BYTES5242880Per-file cap (5 MB). Larger files return 413 ATTACHMENT_TOO_LARGE
ATTACHMENT_MAX_COUNT5Files per message
ATTACHMENT_MAX_REQUEST_BYTES20971520Raw bytes forwarded to Claude per request (20 MB). Older attachments in the history are dropped first to stay under it
ATTACHMENT_QUOTA_IP_BYTES52428800Upload bytes per client IP per UTC day (50 MB). 0 disables
ATTACHMENT_QUOTA_TENANT_BYTES524288000Upload bytes per tenant per UTC day (500 MB). 0 disables
TENANT_IDOrigin hostQuota tenant. Defaults to the embedding site’s host, so each site in ALLOWED_ORIGIN gets its own budget
ATTACHMENT_STORAGEpassthroughpassthrough or r2 (see below)
ATTACHMENT_RETENTION_HOURS24R2 only: how long stored files (and their signed URLs) live
ATTACHMENT_SIGNING_SECRETR2 only: secret used to sign download URLs. Set with npx wrangler secret put ATTACHMENT_SIGNING_SECRET

The worker independently re-validates every attachment: the declared MIME type must be on the allowlist and match the file’s leading bytes, the decoded size is recomputed, and only user messages may carry files.

Quotas use the same KV namespace as rate limiting and count only the new uploads on the latest message. When a quota is hit the worker returns 413 ATTACHMENT_QUOTA_EXCEEDED with a Retry-After pointing at the next UTC midnight.

Bytes are base64-encoded, sent to Anthropic inside the chat request, and discarded. Nothing is written to Cloudflare storage. Because the API is stateless, the widget keeps the bytes in memory and re-sends them with the history on every later turn of that session. That costs upload bandwidth and input tokens for image-heavy conversations, and a page reload loses the bytes (the model then sees a short “no longer available” note in place of the file).

Good for low-volume support chat where you’d rather not hold visitor files at all.

Set ATTACHMENT_STORAGE=r2, bind an R2 bucket as ATTACHMENTS, and set ATTACHMENT_SIGNING_SECRET:

Terminal window
cd worker
npx wrangler r2 bucket create claudius-attachments
npx wrangler secret put ATTACHMENT_SIGNING_SECRET
wrangler.toml
[vars]
ATTACHMENT_STORAGE = "r2"
ATTACHMENT_RETENTION_HOURS = "24"
[[r2_buckets]]
binding = "ATTACHMENTS"
bucket_name = "claudius-attachments"

In R2 mode a new upload is written under att/<tenant>/<uuid>, forwarded to Claude, and returned to the widget as a storage key plus an HMAC-signed download URL. Later turns reference the key, so the file is uploaded once per conversation and previews survive a reload. The worker refuses to serve or forward an object past its expiresAt and deletes it lazily on the next read; add a bucket lifecycle rule as the hard backstop:

Terminal window
npx wrangler r2 bucket lifecycle add claudius-attachments --expire-days 2

(Pick a value at least as long as ATTACHMENT_RETENTION_HOURS.)

Where attachments live

  • Passthrough: in the visitor’s browser tab for the session, in transit to your worker, and in the request to Anthropic. The worker keeps nothing.
  • R2: additionally in your R2 bucket, under a key that names the tenant (embedding site) but not the visitor, for ATTACHMENT_RETENTION_HOURS.

How long

  • Browser: until the tab closes. Persisted history (sessionStorage) stores filenames, sizes, keys, and signed URLs, never the bytes.
  • Anthropic: per their data retention policy for API inputs.
  • R2: ATTACHMENT_RETENTION_HOURS (default 24 h), enforced by the worker and by your lifecycle rule.
  • KV: quota counters expire after 24 h and hold byte totals only.

Who can read them

  • Signed URLs grant read access to anyone who holds the link until it expires. They appear only in chat responses to the widget that uploaded the file; treat them like the conversation itself.
  • Storage keys are unguessable UUIDs. A key alone is enough to reference a stored file on a later turn from the same worker, which is how follow-up questions work.
  • Analytics (ANALYTICS_DB) never records attachment contents or names.

Cover attachments in your own privacy notice: tell visitors that uploaded files are sent to Anthropic for processing and, if you use R2, how long you keep them.

StatuscodeMeaning
400ATTACHMENTS_DISABLEDATTACHMENTS_ENABLED=false
400ATTACHMENT_INVALIDDisallowed type, type/bytes mismatch, too many files, malformed reference, or Claude could not process the file
413ATTACHMENT_TOO_LARGEA file exceeds ATTACHMENT_MAX_BYTES, or the message’s uploads exceed ATTACHMENT_MAX_REQUEST_BYTES
413ATTACHMENT_QUOTA_EXCEEDEDDaily per-IP or per-tenant byte quota reached

The widget shows a localized message for each, removes the rejected message from the conversation, and does not retry. See the REST API reference for the wire format.