Sending Files Using API Calls
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_datain theapi_call_sendmutation (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
*_multipartrequest type - describe the request in anapp/api_callstemplate 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_dataworks withPOST,PUTandPATCH. - Either
bodyorform_data. A request cannot set both. - Content-Type is set for you. The
multipart/form-dataheader and its boundary are generated from the body. AContent-Typeinheadersis ignored forform_datarequests, so a header copied from an API'scurlexample 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.