Uploads Live
Two-step upload for attachments, avatars, voice samples, and KB documents. Step 1 asks our API for a presigned PUT URL; step 2 streams the bytes directly to DigitalOcean Spaces. The resulting public URL is what you attach to messages, profiles, or any resource that takes a file URL.
Presign an upload
Returns a one-time PUT URL valid for 15 minutes, plus the public URL the file will be reachable at after upload. You PUT the bytes directly to the upload URL; the API never sees your file.
| Field | Type | Description |
|---|---|---|
| filenamerequired | string | Original filename. Used in the public URL path. |
| mimeTyperequired | string | Content type. Must match the Content-Type header you PUT with. |
| sizerequired | integer | File size in bytes. Cap is 100MB by default, 25MB for free workspaces. |
| purposeoptional | enum | One of attachment, avatar, voice_sample, kb_doc. Defaults to attachment. |
{
"filename": "refund-policy.pdf",
"mimeType": "application/pdf",
"size": 53120
}{
"uploadUrl": "https://...presigned-put.url",
"uploadHeaders": {
"Content-Type": "application/pdf"
},
"publicUrl": "https://cdn.message.com/u/01HRX0.../refund-policy.pdf",
"expiresAt": "2026-05-13T15:45:00Z"
}Code samples
# Step 1. ask the API for a presigned URL
curl -X POST 'https://app.message.com/api/v1/uploads/presign' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT' \
-H 'Content-Type: application/json' \
-d '{"filename":"refund-policy.pdf","mimeType":"application/pdf","size":53120}'
# Step 2. PUT the bytes directly to the returned URL
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary @./refund-policy.pdf// Step 1: ask for presigned URL
const presign = await fetch('https://app.message.com/api/v1/uploads/presign', {
method: 'POST',
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
mimeType: file.type,
size: file.size
})
}).then(r => r.json());
// Step 2: PUT the file bytes directly
await fetch(presign.uploadUrl, {
method: 'PUT',
headers: presign.uploadHeaders,
body: file
});
// presign.publicUrl is now your attachment URLimport requests
# Step 1: presign
presign = requests.post(
"https://app.message.com/api/v1/uploads/presign",
headers={"Authorization": f"Bearer {token}"},
json={"filename": "refund-policy.pdf", "mimeType": "application/pdf", "size": 53120},
).json()
# Step 2: PUT bytes
with open("refund-policy.pdf", "rb") as f:
requests.put(presign["uploadUrl"], headers=presign["uploadHeaders"], data=f)
public_url = presign["publicUrl"]require "net/http"
require "json"
# Step 1: presign
uri = URI("https://app.message.com/api/v1/uploads/presign")
req = Net::HTTP::Post.new(uri, {
"Authorization" => "Bearer #{token}",
"Content-Type" => "application/json"
})
req.body = { filename: "refund-policy.pdf", mimeType: "application/pdf", size: 53120 }.to_json
presign = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
# Step 2: PUT bytes
put_uri = URI(presign["uploadUrl"])
put_req = Net::HTTP::Put.new(put_uri, presign["uploadHeaders"])
put_req.body = File.read("refund-policy.pdf")
Net::HTTP.start(put_uri.host, put_uri.port, use_ssl: true) { |h| h.request(put_req) }<?php
// Step 1: presign
$ctx = stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer $token\r\nContent-Type: application/json",
"content" => json_encode([
"filename" => "refund-policy.pdf",
"mimeType" => "application/pdf",
"size" => 53120
])
],
]);
$presign = json_decode(file_get_contents("https://app.message.com/api/v1/uploads/presign", false, $ctx), true);
// Step 2: PUT bytes
$ch = curl_init($presign["uploadUrl"]);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => file_get_contents("refund-policy.pdf"),
CURLOPT_HTTPHEADER => ["Content-Type: application/pdf"],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);Direct multipart fallback
For environments that cannot do two-step uploads (mobile WebViews, restrictive CSP), POST multipart/form-data with a single file field. We accept it server-side and stream to storage. Slower; use the presigned path when possible.
Delete an upload
Removes the underlying object from storage. References to it (in messages, profiles, KB) become broken; we do not cascade. Use sparingly.
Limits
| Limit | Value |
|---|---|
| Max file size (Free) | 25 MB |
| Max file size (Pro+) | 100 MB |
| Presign URL lifetime | 15 minutes |
| Allowed MIME types | Most common docs, images, audio, video. Executables blocked. |
The presign URL is single-use and time-limited. Do not cache or share it. If your client retries after expiry, request a fresh presign.
Errors
| Code | When |
|---|---|
400 invalid_body | Size exceeds plan limit or MIME type disallowed. |
402 plan_limit | Workspace storage quota reached. |
413 payload_too_large | Direct multipart upload exceeded size limit. |
Common pitfalls
- PUTting with the wrong
Content-Type. The header must match what you presigned with. Mismatch returns 403 from the storage layer. - Reusing a presign URL. Single-use; second PUT returns 403.
- Attaching unsupported file types. Executables, scripts, and bare archives are blocked. Wrap large code drops in a zip if you must, and consider the security implications.