# POST /files/bundle

**Resource:** [Files](./files.md)  
**Scopes:** `files:read`  
**Write operation:** no

Bundle up to 50 files (any mix of document / image / secure types -- auto-detected) into a ZIP archive. Default mode ("binary") streams the ZIP back directly as application/zip (suitable for browser save dialogs). Pass ?response=url (or "response": "url" in the body) to stage the ZIP and return a 10-minute signed download URL instead -- use this mode with API agents that cannot consume a large binary response. Cap: 50 files / 200MB total decompressed. Files that cannot be fetched from storage are listed in a _manifest.txt included in the ZIP and in the skipped[] field of the url-mode response. Requires files:read scope (no write scope needed).

## Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `ids` | body | string[] | yes | Array of 1-50 file UUIDs to bundle. Mix of document, image, and secure types is fine -- each is auto-detected. |
| `filename` | body | string | no | Optional ZIP filename without extension. Defaults to "attachments_YYYY-MM-DD". A .zip suffix is always appended. |
| `response` | body | string | no | Response mode. "binary" (default) streams the ZIP directly. "url" returns a JSON body with a signed URL good for 10 minutes. Also accepted as a query parameter: ?response=url. |

## Response example

```json
{
  "data": {
    "download_url": "https://trustpager-private.r2.cloudflarestorage.com/_bundles/company-id/uuid.zip?X-Amz-Signature=...",
    "filename": "deal_attachments_2026-05-19.zip",
    "expires_in_seconds": 600,
    "file_count": 3,
    "size_bytes": 567246,
    "skipped": [
      {
        "id": "uuid",
        "name": "missing.pdf",
        "reason": "storage_unavailable"
      }
    ]
  },
  "meta": {
    "credits_remaining": 9800
  }
}
```

---
Base URL: `https://api.trustpager.com/functions/v1/api/v1` — Auth: `Authorization: Bearer YOUR_API_KEY`