When a Source receives a multipart/form-data request, a Transformation receives its form fields as an object. Each file is described by its filename, content_type and size, and has content, a Buffer holding the file's bytes.

You can read files, replace or rename them, add new files, and remove files. This guide converts an uploaded CSV file to a JSON event, then shows how to edit the files in a multipart request.

File content is available on connections that use binary-safe multipart handling. Connections created before October 1, 2026, 18:40 UTC use legacy handling; see the multipart migration guide to move one. For how Hookdeck parses multipart requests, see handle multipart requests.

Convert a CSV upload to JSON

A form uploads a CSV file in its file field, along with a note text field. This transformation parses the CSV with papaparse and replaces the multipart body with JSON.

Write and bundle the transformation

Transformations can only import Node.js built-in modules, so you bundle papaparse into your code with esbuild:

npm install papaparse
npm install --save-dev esbuild

Save this as csv-to-json.js:

import Papa from 'papaparse';

export default {
  transform(request) {
    const { file, ...fields } = request.body;
    const { data, errors } = Papa.parse(file.content.toString('utf8'), {
      header: true,
      dynamicTyping: true,
      skipEmptyLines: true,
    });
    if (errors.length) throw new Error(`Invalid CSV: ${errors[0].message}`);

    request.body = { ...fields, filename: file.filename, rows: data };
    request.headers['content-type'] = 'application/json';
    return request;
  },
};
npx esbuild csv-to-json.js --bundle --format=esm --platform=node --outfile=dist/csv-to-json.js

Paste the contents of dist/csv-to-json.js into a new transformation on your connection. See create a transformation.

Upload a file

curl -X POST "https://hkdk.events/src_xxxxxxxx" \
  -F "note=October export" \
  -F "file=@customers.csv;type=text/csv"

What your destination receives

For a CSV file with two rows, your destination receives Content-Type: application/json and this body:

{
  "note": "October export",
  "filename": "customers.csv",
  "rows": [
    { "email": "jane@example.com", "name": "Jane Doe", "plan": "pro", "seats": 5 },
    { "email": "bob@example.com", "name": "Bob Lee", "plan": "free", "seats": 1 }
  ]
}

dynamicTyping turns numeric values such as seats into numbers. If the file isn't valid CSV, the transformation throws and Hookdeck opens a transformation issue.

Replace, add, and remove files

To keep the request as multipart and change its files, edit request.body and leave the content-type header unchanged. This transformation receives a gzip-compressed report and a debug log. It decompresses the report, adds a summary file, and removes the log:

const { gunzipSync } = require('node:zlib');

addHandler('transform', (request) => {
  const { report } = request.body;

  // Replace a file: decompress report.csv.gz into report.csv
  report.content = gunzipSync(report.content);
  report.filename = report.filename.replace(/\.gz$/, '');
  report.content_type = 'text/csv';

  // Add a file
  const rows = report.content.toString('utf8').trim().split('\n').length - 1;
  request.body.summary = {
    filename: 'summary.json',
    content_type: 'application/json',
    content: JSON.stringify({ rows }),
  };

  // Remove a file
  delete request.body.debug_log;

  return request;
});

Upload the files:

curl -X POST "https://hkdk.events/src_xxxxxxxx" \
  -F "note=nightly" \
  -F "report=@report.csv.gz;type=application/gzip" \
  -F "debug_log=@debug.log;type=text/plain"

Your destination receives a multipart request with the original boundary and three parts: the note field, report as report.csv with Content-Type: text/csv and the decompressed CSV, and summary as summary.json with {"rows":2}.

These rules apply when you edit files:

  • Unchanged files keep their original bytes.
  • Changing a file's content, filename or content_type replaces the file. content can be a Buffer, a Uint8Array or a string, and Hookdeck computes size from the new bytes.
  • Adding a field with an object that has content adds a file. Set filename and content_type with it.
  • Deleting a field removes it, or remove an entry from a repeated field's array to remove one file.

Create a multipart upload from JSON

Some APIs only accept file uploads. To send JSON data as a file, set the content-type header to multipart/form-data and add an object with content:

addHandler('transform', (request) => {
  const { orders } = request.body;
  const csv = ['id,total', ...orders.map((order) => `${order.id},${order.total}`)].join('\n');

  request.headers['content-type'] = 'multipart/form-data';
  request.body = {
    description: 'Daily orders',
    file: { filename: 'orders.csv', content_type: 'text/csv', content: csv },
  };
  return request;
});

For a JSON request with two orders, your destination receives a multipart request with a description text field and a file part named orders.csv, with Content-Type: text/csv:

id,total
1,99.5
2,12

Hookdeck generates the multipart boundary.

Put file bytes in JSON

Changed or new file bytes can only be returned in a multipart body. To include a file in a JSON body, encode it, for example as base64:

addHandler('transform', (request) => {
  const { file, ...fields } = request.body;

  request.body = {
    ...fields,
    file: {
      filename: file.filename,
      content_type: file.content_type,
      data: file.content.toString('base64'),
    },
  };
  request.headers['content-type'] = 'application/json';
  return request;
});

Returning a Buffer inside a JSON body fails the transformation with File content can only be returned in a multipart/form-data body. To include it in another format, encode it, for example with content.toString('base64').

When you convert a multipart request to JSON and keep a file object unchanged, the JSON contains the file's filename, content_type and size, without its content.

Limits and pitfalls

  • Execution time. Transformations must finish within 1 second, including the time spent parsing files.
  • Memory. Each execution has 128 MB of memory, and parsing a file into objects uses much more memory than the file's size.
  • Base64. Base64-encoded files are about a third larger than the original bytes.
  • Text encoding. file.content.toString('utf8') assumes the file is UTF-8. For other encodings, such as Windows-1252, bundle iconv-lite to decode it.
  • Older connections. On connections using legacy multipart handling, files are described by their metadata only and have no content.

For the modules and libraries transformations can use, see Transformation Node.js compatibility.