# Upload attachments

> Attach files to messages, notes and drafts using presigned uploads, register files that stay in your own storage, and download attachments.

Source: https://developer.kustomer.com/docs/guides/attachments

Attachments are created in two steps: first you create an **attachment record** that describes the file, then you either **upload the bytes** with the presigned form Kustomer returns, or point Kustomer at a file you already host. Finally you reference the attachment from a message, note or draft.

```mermaid
sequenceDiagram
  autonumber
  participant You
  participant API as Kustomer API
  participant S as Upload URL
  You->>API: POST /v1/attachments { name, contentType, contentLength }
  API-->>You: 201 { data.id, meta.upload: { url, fields } }
  You->>S: POST multipart/form-data (fields…, file)
  S-->>You: 204 No Content
  You->>API: POST /v1/conversations/{id}/messages { …, attachments: [{ _id, name, contentType, contentLength }] }
```

## 1. Create the attachment record

`POST /v1/attachments` ([reference](https://developer.kustomer.com/api/core-resources/create-attachment))

```bash title="cURL"
curl -X POST "https://acme.api.kustomerapp.com/v1/attachments" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "invoice-4417.pdf", "contentType": "application/pdf", "contentLength": 48213 }'
```

`name`, `contentType` and `contentLength` (in bytes) are required. The response includes the attachment `id` and a temporary upload policy:

```json title="201 Created"
{
  "data": {
    "id": "5a4d1a8a6d92880012ad2923",
    "type": "attachment",
    "attributes": { "name": "invoice-4417.pdf", "contentType": "application/pdf", "contentLength": 48213, "uploaded": false }
  },
  "meta": {
    "upload": {
      "url": "https://…",
      "fields": {
        "key": "attachments/…/5a4d1a8a6d92880012ad2923-invoice-4417.pdf",
        "Content-Type": "application/pdf",
        "Policy": "…",
        "X-Amz-Signature": "…"
      }
    }
  }
}
```

## 2. Upload the file

Send a `multipart/form-data` POST to `meta.upload.url`. Include **every** key in `meta.upload.fields` as a form field, exactly as returned, and add the file **last**, under the name `file`. Don't send your Kustomer `Authorization` header to this URL.

```bash title="cURL"
# Build -F flags from meta.upload.fields, then append the file last
curl -X POST "$UPLOAD_URL" \
  -F "key=$KEY" \
  -F "Content-Type=application/pdf" \
  -F "Policy=$POLICY" \
  -F "X-Amz-Signature=$SIGNATURE" \
  -F "file=@invoice-4417.pdf"
```

```ts title="Node.js"
import { readFile } from 'node:fs/promises'

const bytes = await readFile('invoice-4417.pdf')
const created = await fetch(`${base}/v1/attachments`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ name: 'invoice-4417.pdf', contentType: 'application/pdf', contentLength: bytes.length }),
}).then((r) => r.json())

const { url, fields } = created.meta.upload
const form = new FormData()
for (const [k, v] of Object.entries<string>(fields)) form.append(k, v)
form.append('file', new Blob([bytes], { type: 'application/pdf' }), 'invoice-4417.pdf')

const upload = await fetch(url, { method: 'POST', body: form })
if (!upload.ok) throw new Error(`upload failed: ${upload.status}`)
```

```python title="Python"
with open("invoice-4417.pdf", "rb") as f:
    data = f.read()

created = requests.post(f"{base}/v1/attachments", headers=headers, json={
    "name": "invoice-4417.pdf", "contentType": "application/pdf", "contentLength": len(data),
}).json()

upload = created["meta"]["upload"]
res = requests.post(upload["url"], data=upload["fields"], files={"file": ("invoice-4417.pdf", data, "application/pdf")})
res.raise_for_status()
```

The upload policy expires after a short time, so upload right after creating the record. `contentLength` must match the file you upload.

## 3. Attach it

Reference the attachment when you create the message or note. Messages accept attachment objects:

```json title="POST /v1/conversations/{id}/messages"
{
  "channel": "email",
  "app": "acme-billing",
  "direction": "out",
  "subject": "Your invoice",
  "preview": "Hi Jane, your invoice for order #4417 is attached.",
  "attachments": [
    { "_id": "5a4d1a8a6d92880012ad2923", "name": "invoice-4417.pdf", "contentType": "application/pdf", "contentLength": 48213 }
  ]
}
```

Notes take the same shape. See [Create note within conversation](https://developer.kustomer.com/api/core-resources/create-note-within-conversation).

## Files in your own storage

If files must stay in your storage, register them by URL instead of uploading. Pass `externalUrl`:

```json title="POST /v1/attachments"
{
  "name": "contract.pdf",
  "contentType": "application/pdf",
  "contentLength": 120934,
  "externalUrl": "https://files.acme.example/contracts/8812.pdf",
  "renderMode": "default"
}
```

- No upload policy is returned, and the attachment is marked `uploaded: true` straight away.
- `externalUrl` must be an absolute `https://` URL without credentials in it. Private and loopback hosts are rejected.
- Kustomer doesn't fetch, scan, copy or delete the file. Keeping the URL working is up to you; a signed URL that expires will break the attachment.

| `renderMode`    | Behavior                                                                                                                                                                               |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `default`       | Clients get the URL in `links.related` and can open or download the file.                                                                                                              |
| `metadata_only` | The URL is never returned to clients. Agents and SDKs see a non-interactive placeholder with the file's name and size. Use this for files that must only be opened in your own system. |

## Draft attachments

Drafts have their own attachment endpoint. The response's `links.related` is a presigned URL that you upload to with a `PUT`:

`POST /v1/drafts/{id}/attachments` ([reference](https://developer.kustomer.com/api/core-resources/create-draft-attachment))

```bash
curl --upload-file invoice-4417.pdf "$PRESIGNED_URL"
```

## Downloading attachments

`GET /v1/messages/{message_id}/attachments/{id}` ([reference](https://developer.kustomer.com/api/core-resources/get-attachment-by-message-id-and-attachment-id-inbound-or-outbound))

This works for inbound and outbound message attachments. The response's `links.related` is a temporary URL for the file content. Download it promptly and don't store the URL.

## Limits and tips

- Create the attachment record and upload in one go. Records whose upload never completes stay `uploaded: false`.
- Set `contentType` accurately: agents' browsers use it to preview images and PDFs.
- Attachment uploads count toward your rate limit only for the Kustomer API calls, not the upload itself.
