Send with Attachments
curl --request POST \
--url https://app.tuco.ai/api/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"attachmentUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
],
"message": "<string>"
}
'import requests
url = "https://app.tuco.ai/api/messages"
payload = {
"attachmentUrls": ["<string>"],
"attachmentNames": ["<string>"],
"message": "<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({
attachmentUrls: ['<string>'],
attachmentNames: ['<string>'],
message: '<string>'
})
};
fetch('https://app.tuco.ai/api/messages', 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://app.tuco.ai/api/messages",
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([
'attachmentUrls' => [
'<string>'
],
'attachmentNames' => [
'<string>'
],
'message' => '<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://app.tuco.ai/api/messages"
payload := strings.NewReader("{\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"message\": \"<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://app.tuco.ai/api/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"message\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/messages")
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 \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"message\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyMessages
Send with Attachments
Send images, videos, voice notes, and files via iMessage — with or without text. REST endpoint in the Tuco AI iMessage API — bearer-token auth, JSON.
POST
/
api
/
messages
Send with Attachments
curl --request POST \
--url https://app.tuco.ai/api/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"attachmentUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
],
"message": "<string>"
}
'import requests
url = "https://app.tuco.ai/api/messages"
payload = {
"attachmentUrls": ["<string>"],
"attachmentNames": ["<string>"],
"message": "<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({
attachmentUrls: ['<string>'],
attachmentNames: ['<string>'],
message: '<string>'
})
};
fetch('https://app.tuco.ai/api/messages', 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://app.tuco.ai/api/messages",
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([
'attachmentUrls' => [
'<string>'
],
'attachmentNames' => [
'<string>'
],
'message' => '<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://app.tuco.ai/api/messages"
payload := strings.NewReader("{\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"message\": \"<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://app.tuco.ai/api/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"message\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/messages")
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 \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"message\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodySend attachments alongside or instead of text messages. Tuco handles downloading, caching, and delivering files through your device relay.
Request body
Use the samePOST /api/messages endpoint. Add attachmentUrls to your request:
string[]
required
Array of public URLs to attach. Max 2 attachments per message, max 25 MB each.
string[]
Optional display names for each attachment (same order as
attachmentUrls). When omitted, the filename from the URL is used.string
Optional text to send with the attachments. When omitted, only the attachment is sent (no “Attachment” placeholder text).
All other
POST /api/messages body fields
apply here too — including forceFallback
(skip the iMessage availability check and route straight to your configured
fallback). One caveat: the SMS/fallback channel is text-only, so a forceFallback
send delivers the message text but not the attachments — those go over
iMessage only.Supported formats
| Category | Formats | Notes |
|---|---|---|
| Images | PNG, JPG, JPEG, GIF, WebP, HEIC | Renders inline in iMessage |
| Video | MP4, MOV | Up to 25 MB. Renders as playable video |
| Audio / Voice notes | CAF | .caf files render as native iMessage voice notes (the play button bubble). Record with Core Audio or convert from other formats |
| Documents | Renders as attachment preview |
Voice notes must be
.caf format. MP3, WAV, M4A, and other audio formats are sent as generic file attachments, not as the native iMessage voice note bubble. To get the recognizable play-button bubble, use .caf (Core Audio Format).Examples
curl -X POST "https://app.tuco.ai/api/messages" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"message": "Check this out!",
"leadId": "667f1f77bcf86cd799439012",
"attachmentUrls": ["https://example.com/photo.jpg"]
}'
curl -X POST "https://app.tuco.ai/api/messages" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"leadId": "667f1f77bcf86cd799439012",
"attachmentUrls": ["https://example.com/intro-video.mp4"]
}'
curl -X POST "https://app.tuco.ai/api/messages" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"leadId": "667f1f77bcf86cd799439012",
"attachmentUrls": ["https://example.com/voice-note.caf"]
}'
curl -X POST "https://app.tuco.ai/api/messages" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"message": "I made this for you. Whenever you are ready: example.com/schedule",
"leadId": "667f1f77bcf86cd799439012",
"attachmentUrls": ["https://example.com/personalized-video.mp4"]
}'
Limits
| Limit | Value |
|---|---|
| Max attachments per message | 2 |
| Max file size per attachment | 25 MB |
| Supported sources | Any public URL, UploadThing URLs, Blob URLs belonging to your workspace |
Attachment caching: When you send the same attachment URL multiple times (e.g. a promo video in a campaign), Tuco caches it per line. The second send skips download + upload, making delivery significantly faster.
Device gap
Each attachment counts as part of a single message — not a separate send. Sending text + video in one API call uses one device gap (30s), not two. This is significantly faster than sending them as separate messages.For automations that send a video followed by a text CTA, combine them into a single API call with
message + attachmentUrls instead of two separate calls. This halves your device gap overhead.Error responses
| Status | When | Body |
|---|---|---|
400 | attachmentUrls not an array | { "error": "attachmentUrls must be an array" } |
400 | More than 2 attachments | { "error": "Maximum 2 attachments per message" } |
403 | URL doesn’t belong to workspace | { "error": "Attachment URL does not belong to this organization" } |