curl --request POST \
--url https://api.example.com/files/upload-init \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"filename": "<string>",
"content_type": "application/octet-stream",
"size_bytes": 123,
"folder_id": "<string>",
"tags": [],
"description": "<string>",
"entity_type": "<string>",
"entity_id": "<string>"
}
'import requests
url = "https://api.example.com/files/upload-init"
payload = {
"filename": "<string>",
"content_type": "application/octet-stream",
"size_bytes": 123,
"folder_id": "<string>",
"tags": [],
"description": "<string>",
"entity_type": "<string>",
"entity_id": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
filename: '<string>',
content_type: 'application/octet-stream',
size_bytes: 123,
folder_id: '<string>',
tags: [],
description: '<string>',
entity_type: '<string>',
entity_id: '<string>'
})
};
fetch('https://api.example.com/files/upload-init', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/files/upload-init",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'filename' => '<string>',
'content_type' => 'application/octet-stream',
'size_bytes' => 123,
'folder_id' => '<string>',
'tags' => [
],
'description' => '<string>',
'entity_type' => '<string>',
'entity_id' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/files/upload-init"
payload := strings.NewReader("{\n \"filename\": \"<string>\",\n \"content_type\": \"application/octet-stream\",\n \"size_bytes\": 123,\n \"folder_id\": \"<string>\",\n \"tags\": [],\n \"description\": \"<string>\",\n \"entity_type\": \"<string>\",\n \"entity_id\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/files/upload-init")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"filename\": \"<string>\",\n \"content_type\": \"application/octet-stream\",\n \"size_bytes\": 123,\n \"folder_id\": \"<string>\",\n \"tags\": [],\n \"description\": \"<string>\",\n \"entity_type\": \"<string>\",\n \"entity_id\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/files/upload-init")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"filename\": \"<string>\",\n \"content_type\": \"application/octet-stream\",\n \"size_bytes\": 123,\n \"folder_id\": \"<string>\",\n \"tags\": [],\n \"description\": \"<string>\",\n \"entity_type\": \"<string>\",\n \"entity_id\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Initiate an Upload
Step 1 of the two-step upload flow.
Creates the files metadata row and returns a presigned PUT URL. The browser must PUT the file bytes directly to upload_url. On success the frontend should call onUploadComplete with the file_id.
curl --request POST \
--url https://api.example.com/files/upload-init \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"filename": "<string>",
"content_type": "application/octet-stream",
"size_bytes": 123,
"folder_id": "<string>",
"tags": [],
"description": "<string>",
"entity_type": "<string>",
"entity_id": "<string>"
}
'import requests
url = "https://api.example.com/files/upload-init"
payload = {
"filename": "<string>",
"content_type": "application/octet-stream",
"size_bytes": 123,
"folder_id": "<string>",
"tags": [],
"description": "<string>",
"entity_type": "<string>",
"entity_id": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
filename: '<string>',
content_type: 'application/octet-stream',
size_bytes: 123,
folder_id: '<string>',
tags: [],
description: '<string>',
entity_type: '<string>',
entity_id: '<string>'
})
};
fetch('https://api.example.com/files/upload-init', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/files/upload-init",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'filename' => '<string>',
'content_type' => 'application/octet-stream',
'size_bytes' => 123,
'folder_id' => '<string>',
'tags' => [
],
'description' => '<string>',
'entity_type' => '<string>',
'entity_id' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/files/upload-init"
payload := strings.NewReader("{\n \"filename\": \"<string>\",\n \"content_type\": \"application/octet-stream\",\n \"size_bytes\": 123,\n \"folder_id\": \"<string>\",\n \"tags\": [],\n \"description\": \"<string>\",\n \"entity_type\": \"<string>\",\n \"entity_id\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/files/upload-init")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"filename\": \"<string>\",\n \"content_type\": \"application/octet-stream\",\n \"size_bytes\": 123,\n \"folder_id\": \"<string>\",\n \"tags\": [],\n \"description\": \"<string>\",\n \"entity_type\": \"<string>\",\n \"entity_id\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/files/upload-init")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"filename\": \"<string>\",\n \"content_type\": \"application/octet-stream\",\n \"size_bytes\": 123,\n \"folder_id\": \"<string>\",\n \"tags\": [],\n \"description\": \"<string>\",\n \"entity_type\": \"<string>\",\n \"entity_id\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}files row with status: "pending" and returns a presigned PUT URL valid for 15 minutes. The caller uploads the raw bytes directly to upload_url, then calls Confirm an Upload, which verifies the object landed and flips the row to status: "uploaded".
Get a Download URL and Get Extracted Text Content reject a still-pending file with 409. Generate an AI Description does not — it never checks status, and on a pending file the missing S3 object is swallowed during text extraction, so it returns 200 with a description derived from the filename alone and persists it. Confirm the upload before asking for a description.
The presigned upload URL is signed against the exact content_type you send here. If the client’s PUT request uses a different Content-Type header than what was passed to this call, S3 rejects the request with a signature mismatch — the header must match exactly, not be inferred by the HTTP client.
Attachments are a many-to-many join, not a field on the file (see Attach a File to an Entity). Setting entity_type and entity_id here creates the first attachment inline as part of this same call — but setting only one of the pair is a silent no-op, no attachment is created and no error is raised.
Auth
Requires a CRM manage scope and an active organization on the token. Any*:manage scope qualifies — in practice contacts:manage, deals:manage, companies:manage, or activities:manage.
Response
| Field | Type | Description |
|---|---|---|
upload_url | string | Presigned S3 PUT URL, expires in 900 seconds. |
key | string | The S3 object key the client must PUT to. Same value as file.s3_key. |
file_id | string (uuid) | The new file’s id. |
file | object | The full created files row (status: "pending"). |
Errors
| Status | Cause |
|---|---|
404 Not Found | folder_id doesn’t exist (or isn’t in this org / is archived). |
502 Bad Gateway | Could not generate the presigned URL, or the database insert failed. |
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
Original filename; sanitized and embedded in the generated S3 key.
MIME type stored on the file row and set as the presigned PUT's Content-Type.
Client-reported size in bytes; stored as-is, not verified against the uploaded object.
Destination folder id; omit to upload to the Files root.
Tag strings to store on the file row.
Optional one-line description; can also be filled in later via generate-description.
If set together with entity_id, attaches the file to this CRM entity as part of the same call.
If set together with entity_type, attaches the file to this CRM entity as part of the same call.
Response
Successful Response
The response is of type Response Upload Init Files Upload Init Post · object.