|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +title: "File Data Delivery" |
| 4 | +--- |
| 5 | + |
| 6 | +# File Data Delivery |
| 7 | + |
| 8 | +[[API Docs](/api/webhooks)] |
| 9 | +[[SDK](https://www.npmjs.com/package/@epilot/webhooks-client)] |
| 10 | + |
| 11 | +Some events carry one or more files — for example **File Created** (`event_FileCreated`). For these **file events**, a webhook can deliver the actual file *bytes* to your endpoint, not just a reference to them. |
| 12 | + |
| 13 | +You choose how the bytes are delivered with the webhook's `deliveryMode`: |
| 14 | + |
| 15 | +- **`json_base64`** — each file is Base64-encoded and embedded in the JSON payload. Supports JSONata transformation. |
| 16 | +- **`binary_multipart`** — files are sent as binary parts in a `multipart/form-data` request. Best for ERP systems and large files. |
| 17 | + |
| 18 | +:::info |
| 19 | +`deliveryMode` is **only relevant for file events**. For all other events it is absent and has no effect. A webhook is a file event when its event payload contains an `event_attachments` array (see below). |
| 20 | +::: |
| 21 | + |
| 22 | +## What is a file event? |
| 23 | + |
| 24 | +File events are delivered using the [Event Catalog](/api/event-catalog) envelope (fields such as `_event_name`, `_event_id`, `_org_id`, and the hydrated entity), not the `metadata`/`entity` structure of [automation-trigger webhooks](./automation-trigger.md). What makes an event a *file event* is a top-level `event_attachments` array. Each entry describes one file: |
| 25 | + |
| 26 | +| Field | Type | Description | |
| 27 | +| --------------- | -------- | ------------------------------------------------------------------ | |
| 28 | +| `entity_id` | string | Entity ID of the file | |
| 29 | +| `filename` | string? | Name of the file (e.g. `invoice.pdf`) | |
| 30 | +| `mime_type` | string? | MIME type (e.g. `application/pdf`) | |
| 31 | +| `size_bytes` | number? | File size in bytes | |
| 32 | +| `readable_size` | string? | Human-readable size (e.g. `"200 KB"`) | |
| 33 | +| `s3ref` | object? | Internal storage reference (`bucket`, `key`) — not directly accessible to you | |
| 34 | +| `version_index` | number | File version index (`0` for newly created files) | |
| 35 | + |
| 36 | +```json title="File event payload (abbreviated)" |
| 37 | +{ |
| 38 | + "_event_name": "FileCreated", |
| 39 | + "_event_id": "01F130Q52Q6MWSNS8N2AVXV4JN", |
| 40 | + "_event_time": "2026-06-18T10:00:00.000Z", |
| 41 | + "_org_id": "739224", |
| 42 | + "operation": "createEntity", |
| 43 | + "event_attachments": [ |
| 44 | + { |
| 45 | + "entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", |
| 46 | + "filename": "invoice.pdf", |
| 47 | + "mime_type": "application/pdf", |
| 48 | + "size_bytes": 204800, |
| 49 | + "readable_size": "200 KB", |
| 50 | + "version_index": 0 |
| 51 | + } |
| 52 | + ] |
| 53 | + // ...plus the hydrated entity (e.g. "file": { ... }) and other event fields |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +## Default: references only |
| 58 | + |
| 59 | +If you do **not** set a `deliveryMode`, the webhook is delivered as a normal JSON payload. The `event_attachments` references are included, but **the file binaries are not loaded or transmitted** — your endpoint receives the references only and is responsible for fetching the files itself if needed. |
| 60 | + |
| 61 | +To deliver the actual bytes, opt in by choosing one of the two delivery modes below. In the webhook configuration UI this is the *"This event can include attached files — include them in the payload?"* setting. |
| 62 | + |
| 63 | +## Mode 1 — JSON (Base64) |
| 64 | + |
| 65 | +Set `deliveryMode: "json_base64"`. Each attached file is downloaded, Base64-encoded, and appended to the payload as a `file_data` array. The original `event_attachments` references remain in place. |
| 66 | + |
| 67 | +Each `file_data` entry contains: |
| 68 | + |
| 69 | +| Field | Type | Description | |
| 70 | +| ------------ | ------ | ------------------------------------ | |
| 71 | +| `base64` | string | The file bytes, Base64-encoded | |
| 72 | +| `mime_type` | string | MIME type of the file | |
| 73 | +| `filename` | string | Original filename | |
| 74 | +| `size_bytes` | number | File size in bytes | |
| 75 | + |
| 76 | +```json title="json_base64 payload" |
| 77 | +{ |
| 78 | + "_event_name": "FileCreated", |
| 79 | + "_org_id": "739224", |
| 80 | + "event_attachments": [ |
| 81 | + { "entity_id": "3fa85f64-...", "filename": "invoice.pdf", "mime_type": "application/pdf", "size_bytes": 204800, "version_index": 0 } |
| 82 | + ], |
| 83 | + "file_data": [ |
| 84 | + { |
| 85 | + "base64": "JVBERi0xLjQKJcfsj6IK...", |
| 86 | + "mime_type": "application/pdf", |
| 87 | + "filename": "invoice.pdf", |
| 88 | + "size_bytes": 204800 |
| 89 | + } |
| 90 | + ] |
| 91 | + // ...plus other event fields |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +The request is sent as `application/json`. A [JSONata transformation](./customization.md) — if configured — runs **after** `file_data` is added, so your expression can reference the encoded files. |
| 96 | + |
| 97 | +## Mode 2 — Binary (multipart/form-data) |
| 98 | + |
| 99 | +Set `deliveryMode: "binary_multipart"`. Files are sent as binary parts in a `multipart/form-data` request (`Content-Type: multipart/form-data; boundary=...`). This avoids the ~33% size overhead of Base64 and is the format most ERP systems and file-import endpoints expect. |
| 100 | + |
| 101 | +The body is shaped by the optional `multipartConfig` object: |
| 102 | + |
| 103 | +| Field | Default | Description | |
| 104 | +| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | |
| 105 | +| `fileFieldName` | `"file"` | Name of the binary form part. The same name is reused for every part when multiple files are sent. | |
| 106 | +| `fileFieldStrategy` | `"single"` | `single` fails the delivery if the picker resolves to more than one attachment. `multi` accepts any non-empty count. | |
| 107 | +| `fileSource` | `"event_attachments"` | JSONata expression selecting which attachment(s) to send. When it resolves to `undefined`, `null`, or `[]`, the event is silently skipped. Validated at save time. | |
| 108 | +| `extraFields` | `{}` | Map of `formFieldName → JSONata expression` for additional **scalar** form parts (e.g. an entity reference). | |
| 109 | + |
| 110 | +```text title="binary_multipart request body" |
| 111 | +--boundary |
| 112 | +Content-Disposition: form-data; name="file"; filename="invoice.pdf" |
| 113 | +Content-Type: application/pdf |
| 114 | +
|
| 115 | +<binary bytes> |
| 116 | +--boundary |
| 117 | +Content-Disposition: form-data; name="entityId" |
| 118 | +
|
| 119 | +3fa85f64-5717-4562-b3fc-2c963f66afa6 |
| 120 | +--boundary-- |
| 121 | +``` |
| 122 | + |
| 123 | +### Picking files with `fileSource` |
| 124 | + |
| 125 | +`fileSource` is a JSONata expression evaluated against the event payload. Use it to filter or select attachments: |
| 126 | + |
| 127 | +```text title="fileSource examples" |
| 128 | +event_attachments // all attachments (default) |
| 129 | +event_attachments[0] // only the first |
| 130 | +event_attachments[mime_type='application/pdf'] // only PDFs |
| 131 | +event_attachments[mime_type='application/pdf'][0] |
| 132 | +``` |
| 133 | + |
| 134 | +If `fileFieldStrategy` is `single` (the default) and `fileSource` returns more than one attachment, the delivery **fails** — narrow the expression (e.g. add `[0]`) or switch to `multi`. |
| 135 | + |
| 136 | +### Adding scalar fields with `extraFields` |
| 137 | + |
| 138 | +Each `extraFields` value is a JSONata expression evaluated against the event payload, then passed through [environment-secret resolution](../environments-secrets.md) (the same as custom headers). A few rules: |
| 139 | + |
| 140 | +- **Quote static literals.** A bare `"business_partner"` is treated as a path lookup and yields `undefined`. For a literal value, single-quote it *inside* the JSON string: `"'business_partner'"`. |
| 141 | +- Expressions yielding `undefined` or `null` silently omit the field. |
| 142 | +- Object/array results are JSON-stringified before being appended. |
| 143 | + |
| 144 | +```json title="multipartConfig example" |
| 145 | +{ |
| 146 | + "deliveryMode": "binary_multipart", |
| 147 | + "multipartConfig": { |
| 148 | + "fileFieldName": "file", |
| 149 | + "fileFieldStrategy": "single", |
| 150 | + "fileSource": "event_attachments[0]", |
| 151 | + "extraFields": { |
| 152 | + "entityId": "event_attachments[0].entity_id", |
| 153 | + "entityType": "'business_partner'" |
| 154 | + } |
| 155 | + } |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +:::note |
| 160 | +The main `jsonataExpression` does **not** apply in `binary_multipart` mode — shape the body with `fileSource` and `extraFields` instead. |
| 161 | +::: |
| 162 | + |
| 163 | +## Choosing a mode |
| 164 | + |
| 165 | +| | `json_base64` | `binary_multipart` | |
| 166 | +| --- | --- | --- | |
| 167 | +| Transport | JSON (`application/json`) | `multipart/form-data` | |
| 168 | +| Size overhead | ~33% (Base64) | None (raw bytes) | |
| 169 | +| File metadata | In each `file_data` entry | Multipart part headers (`filename`, `Content-Type`) + your `extraFields` | |
| 170 | +| JSONata transform | Yes (full payload) | Only via `fileSource` / `extraFields` | |
| 171 | +| Best for | Lightweight integrations, small files | ERP systems, large files | |
| 172 | + |
| 173 | +## Limits and behavior |
| 174 | + |
| 175 | +- **Maximum file size: 100 MB per file.** Larger files are rejected with HTTP status `413` and code `FILE_TOO_LARGE`; the delivery is not retried. |
| 176 | +- **No attachments → skipped.** If a file event arrives with no matching attachments, no HTTP request is sent and no error notification is raised — the event is marked *skipped* (`NO_FILE_ATTACHMENTS`). |
| 177 | +- Files are buffered in memory during delivery, which is why the 100 MB ceiling applies regardless of mode. |
| 178 | + |
| 179 | +## Signature verification |
| 180 | + |
| 181 | +Both modes are signed using the [Standard Webhooks](../security.md) `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. |
| 182 | + |
| 183 | +- **`json_base64`** is signed over the full JSON request body — verify it exactly like any other webhook (see [Security](../security.md)). |
| 184 | +- **`binary_multipart`** cannot be signed over the raw body, because the multipart boundary is randomized. Instead, epilot signs a **deterministic content string** derived from the file bytes and their metadata. See [Verifying multipart signatures](../security.md#verifying-multipart-signatures). |
| 185 | + |
| 186 | +:::caution |
| 187 | +`extraFields` values are **not** part of the signed content. Do not rely on them for security-sensitive data. |
| 188 | +::: |
0 commit comments