Homepage

Sending Files Using API Calls

Last edit: Oct 06, 2026

This guide will help you send files to an external API as a multipart/form-data request - the format a browser uses for a form with a file input, and the one most upload endpoints expect.

Requirements

This is an advanced tutorial. To follow it, you should be familiar with basic platformOS concepts, HTML, Liquid, APIs and Forms, and the topics in the Get Started section, especially topics related to Notifications.

Choosing an approach

There are two ways to send a multipart request:

  • form_data in the api_call_send mutation (recommended) - describe the request directly in GraphQL. Parts can be files or text values with their own content type, they are sent in the order you list them, names may repeat, and a file can come from a URL or from base64 content generated in Liquid.
  • An API Call Notification with a *_multipart request type - describe the request in an app/api_calls template file. It sends files by URL only, with no text parts, and each file needs a unique name.

Sending files with form_data

Step 1: Create the mutation

Pass the request details in the api_call argument of the api_call_send mutation, with form_data in place of body. This example uploads a photo together with a JSON description of it, as an API that stores documents against a record might expect:

app/graphql/api_calls/upload_document.graphql
mutation upload_document(
  $url: String!
  $headers: HashObject
  $file_url: String!
  $file_name: String!
  $file_info: String!
) {
  api_call_send(
    api_call: {
      url: $url
      method: "POST"
      headers: $headers
      form_data: [
        { name: "uploadedFile", file: { url: $file_url, filename: $file_name, content_type: "image/jpeg" } }
        { name: "fileInfo", value: $file_info, content_type: "application/json" }
      ]
    }
  ) {
    response {
      status
      body
    }
    errors {
      message
    }
  }
}

Each element of form_data is one part of the request body:

Field Description
name Required. The form field name. Names may repeat, e.g. files[].
value A text value, sent verbatim.
content_type The Content-Type of a value part, e.g. application/json. Omitted from the part when not set.
file A file.

A part sets exactly one of value or file. A file sets exactly one of url or content_base64:

Field Description
url The URL the file is downloaded from before the request is sent, e.g. the URL of an uploaded property.
content_base64 The file content, base64-encoded - for files generated in Liquid.
filename The filename the external API receives. Defaults to the last segment of url, or file.
content_type The Content-Type of the file. Defaults to application/octet-stream.

Step 2: Invoke the mutation


{% parse_json headers %}
  { "Authorization": "Bearer {{ context.constants.DOCUMENTS_API_TOKEN }}" }
{% endparse_json %}

{% parse_json file_info %}
  [{ "fileName": "photo.jpeg", "description": "Front of the building" }]
{% endparse_json %}
{% assign file_info_json = file_info | json %}

{% graphql result = 'api_calls/upload_document',
  url: 'https://api.example.com/records/42/documents',
  headers: headers,
  file_url: record.properties.photo.url,
  file_name: 'photo.jpeg',
  file_info: file_info_json
%}

The request the external API receives contains the photo, followed by the JSON part:

POST /records/42/documents HTTP/1.1
Authorization: Bearer ...
Content-Type: multipart/form-data; boundary=...

--...
Content-Disposition: form-data; name="uploadedFile"; filename="photo.jpeg"
Content-Type: image/jpeg

<file content>
--...
Content-Disposition: form-data; name="fileInfo"
Content-Type: application/json

[{"fileName":"photo.jpeg","description":"Front of the building"}]
--...--

Sending a file generated in Liquid

A file that does not exist anywhere yet, such as a CSV report, can be sent as base64 content instead of a URL:


{% capture csv %}id,name
1,Michael
2,Anna
{% endcapture %}
{% assign content = csv | base64_encode %}

{% graphql result, content: content %}
  mutation upload_report($content: String!) {
    api_call_send(
      api_call: {
        url: "https://api.example.com/reports"
        method: "PUT"
        form_data: [{ name: "report", file: { content_base64: $content, filename: "report.csv", content_type: "text/csv" } }]
      }
    ) {
      response { status }
      errors { message }
    }
  }
{% endgraphql %}

Things to know

  • Methods. form_data works with POST, PUT and PATCH.
  • Either body or form_data. A request cannot set both.
  • Content-Type is set for you. The multipart/form-data header and its boundary are generated from the body. A Content-Type in headers is ignored for form_data requests, so a header copied from an API's curl example does not break the request.
  • Limits. A request can have up to 100 parts and download up to 10 files by url. All files and values of one request share a 50MB limit. See Limitations.
  • Logs. The Sent Notifications entry for the request records the part names, file URLs, filenames and content types, and shortened text values - never the content of a file.

Sending files with an API Call Notification

Step 1: Create API Call Notification

app/api_calls/send_file.liquid
---
name: send_file
to: 'https://example.com/endpoint'
format: http
request_type: post_multipart
---
{
  "file": {
    "url": "https://domain.com/my-file.jpg",
    "name": "my-file.jpg",
    "content_type": "image/jpeg"
  },
  "file2": {
    "url": "https://domain.com/other-file.jpg",
    "name": "other-file.jpg",
    "content_type": "image/jpeg"
  }
}

This defines a POST request to the endpoint, which would send two binary files. The main difference between a regular API call and sending a binary file is the _multipart suffix in request type. Valid request types are post_multipart, patch_multipart and put_multipart. The required body format for *_multipart requests is JSON following structure:

{
  "<param name>": {
    "url": "<required url to file remote location>",
    "name": "<optional file name>",
    "content_type": "<optional content type>"
  }
}

All files of one request share a 20MB limit.

Warning

If url includes special characters, for example & [which is the case for private uploads] please make sure to use the html_safe filter.

Questions?

We are always happy to help with any questions you may have.

contact us