message.comDevelopers

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

POST/api/v1/uploads/presignAuth: Bearer

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.

FieldTypeDescription
filenamerequiredstringOriginal filename. Used in the public URL path.
mimeTyperequiredstringContent type. Must match the Content-Type header you PUT with.
sizerequiredintegerFile size in bytes. Cap is 100MB by default, 25MB for free workspaces.
purposeoptionalenumOne of attachment, avatar, voice_sample, kb_doc. Defaults to attachment.
Body
{
  "filename": "refund-policy.pdf",
  "mimeType": "application/pdf",
  "size": 53120
}
200 OK
{
  "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

cURL
# 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
JavaScript
// 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 URL
Python
import 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"]
Ruby
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
<?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

POST/api/v1/uploadsAuth: Bearer

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

DELETE/api/v1/uploads/:idAuth: Bearer

Removes the underlying object from storage. References to it (in messages, profiles, KB) become broken; we do not cascade. Use sparingly.

Limits

LimitValue
Max file size (Free)25 MB
Max file size (Pro+)100 MB
Presign URL lifetime15 minutes
Allowed MIME typesMost 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

CodeWhen
400 invalid_bodySize exceeds plan limit or MIME type disallowed.
402 plan_limitWorkspace storage quota reached.
413 payload_too_largeDirect 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.