Send to a Group Chat
curl --request POST \
--url https://app.tuco.ai/api/unibox/send-group \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"conversationId": "<string>",
"message": "<string>",
"attachmentUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
],
"voiceNote": true
}
'import requests
url = "https://app.tuco.ai/api/unibox/send-group"
payload = {
"conversationId": "<string>",
"message": "<string>",
"attachmentUrls": ["<string>"],
"attachmentNames": ["<string>"],
"voiceNote": True
}
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({
conversationId: '<string>',
message: '<string>',
attachmentUrls: ['<string>'],
attachmentNames: ['<string>'],
voiceNote: true
})
};
fetch('https://app.tuco.ai/api/unibox/send-group', 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/unibox/send-group",
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([
'conversationId' => '<string>',
'message' => '<string>',
'attachmentUrls' => [
'<string>'
],
'attachmentNames' => [
'<string>'
],
'voiceNote' => true
]),
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/unibox/send-group"
payload := strings.NewReader("{\n \"conversationId\": \"<string>\",\n \"message\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"voiceNote\": true\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/unibox/send-group")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"conversationId\": \"<string>\",\n \"message\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"voiceNote\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/unibox/send-group")
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 \"conversationId\": \"<string>\",\n \"message\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"voiceNote\": true\n}"
response = http.request(request)
puts response.read_bodyMessages
Send to a Group Chat
Send text, files or a voice note into an iMessage group chat your line is in. Bearer-token auth, JSON.
POST
/
api
/
unibox
/
send-group
Send to a Group Chat
curl --request POST \
--url https://app.tuco.ai/api/unibox/send-group \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"conversationId": "<string>",
"message": "<string>",
"attachmentUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
],
"voiceNote": true
}
'import requests
url = "https://app.tuco.ai/api/unibox/send-group"
payload = {
"conversationId": "<string>",
"message": "<string>",
"attachmentUrls": ["<string>"],
"attachmentNames": ["<string>"],
"voiceNote": True
}
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({
conversationId: '<string>',
message: '<string>',
attachmentUrls: ['<string>'],
attachmentNames: ['<string>'],
voiceNote: true
})
};
fetch('https://app.tuco.ai/api/unibox/send-group', 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/unibox/send-group",
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([
'conversationId' => '<string>',
'message' => '<string>',
'attachmentUrls' => [
'<string>'
],
'attachmentNames' => [
'<string>'
],
'voiceNote' => true
]),
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/unibox/send-group"
payload := strings.NewReader("{\n \"conversationId\": \"<string>\",\n \"message\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"voiceNote\": true\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/unibox/send-group")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"conversationId\": \"<string>\",\n \"message\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"voiceNote\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/unibox/send-group")
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 \"conversationId\": \"<string>\",\n \"message\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"attachmentNames\": [\n \"<string>\"\n ],\n \"voiceNote\": true\n}"
response = http.request(request)
puts response.read_bodyYou can only send into a group your line is already in. Someone else creates the
group and adds your line. Tuco cannot start a new group.
What this endpoint does
It sends one message into an existing iMessage group chat:- text
- up to 2 files (photo, video, PDF, document, audio)
- or a native voice note (the waveform bubble)
1:1 send (/api/messages) | Group send (this endpoint) | |
|---|---|---|
| Who it goes to | One lead | Everyone in the group |
| Lead / campaign / CRM sync | Yes | No |
| SMS fallback | Yes | No |
| Daily / new-conversation limits | Yes | No |
| Webhooks | message.* | group.message.* |
Authentication
Pass your workspace API key as a Bearer token.Authorization: Bearer tuco_xxxxxxxxxxxxx
Step 1 — find the group’s conversationId
List your conversations. Groups have "isGroup": true.
See List Conversations.
curl "https://app.tuco.ai/api/unibox/conversations?limit=50&messageLimit=1" \
-H "Authorization: Bearer tuco_xxxxxxxxxxxxx"
{
"id": "group:iMessage;+;chat606535035660201399",
"isGroup": true,
"recipientName": "Alex · +13025550100, Sam · sam@example.com",
"groupMembers": [
{ "address": "+13025550100", "name": "Alex" },
{ "address": "sam@example.com", "name": "Sam" }
]
}
id (it starts with group:) is the conversationId you send to.
Step 2 — send
Request body
string
required
The group’s
id from Step 1, for example group:iMessage;+;chat606535035660201399.string
The text. Up to 5,000 characters. Optional when you send a file.
string[]
Up to 2 public
https:// file URLs.string[]
File names, in the same order as
attachmentUrls. Optional. Defaults to the file name in the URL.boolean
default:"false"
true sends one audio file as a native iMessage voice note.
Send exactly one audio URL in attachmentUrls (MP3, M4A, WAV, AAC, OGG, OPUS, FLAC, AIFF, AMR or CAF).
Tuco converts it for you. Needs a line that supports native voice notes.
See voice note requirements.message, a file, or both.
Examples
curl -X POST "https://app.tuco.ai/api/unibox/send-group" \
-H "Authorization: Bearer tuco_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"conversationId": "group:iMessage;+;chat606535035660201399",
"message": "Hi all, sharing the deck now"
}'
curl -X POST "https://app.tuco.ai/api/unibox/send-group" \
-H "Authorization: Bearer tuco_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"conversationId": "group:iMessage;+;chat606535035660201399",
"message": "Here is the deck",
"attachmentUrls": ["https://example.com/files/deck.pdf"],
"attachmentNames": ["deck.pdf"]
}'
curl -X POST "https://app.tuco.ai/api/unibox/send-group" \
-H "Authorization: Bearer tuco_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"conversationId": "group:iMessage;+;chat606535035660201399",
"attachmentUrls": ["https://example.com/audio/hello.m4a"],
"voiceNote": true
}'
Response
Text or files — 200 OK
Sent right away. 200 means your line accepted every part.
{
"success": true,
"messageId": "6aedeb65fa7ac0d2e5e221d5",
"message": {
"_id": "6aedeb65fa7ac0d2e5e221d5",
"direction": "sent",
"status": "sent",
"message": "Hi all, sharing the deck now",
"isGroupChat": true,
"groupChatGuid": "iMessage;+;chat606535035660201399",
"externalMessageId": "626E6E86-…"
}
}
Voice note — 200 OK
A voice note is queued and answered at once, the same as a 1:1 voice note.
It usually arrives within a few seconds. If you also sent message, the text
goes first as its own message, and its id is in textMessageId.
{
"success": true,
"messageId": "6aedeb5cfa7ac0d2e5e221cc",
"status": "sent",
"fastAck": true,
"textMessageId": "6aedeb5bfa7ac0d2e5e221cf",
"message": { "_id": "6aedeb5cfa7ac0d2e5e221cc", "voiceNote": true, "isGroupChat": true }
}
Errors
| Status | When | Body |
|---|---|---|
400 | conversationId is not a group id | { "error": "Not a group conversation" } |
400 | No text and no file | { "error": "Message is empty" } |
400 | Text over 5,000 characters | { "error": "Message is too long (max 5000 characters)" } |
400 | More than 2 files | { "error": "Up to 2 attachments per message" } |
400 | A file URL is not https:// | { "error": "Attachment URL must be https" } |
400 | voiceNote: true with no file | { "code": "VOICE_NOTE_NO_ATTACHMENT", "error": "..." } |
400 | voiceNote: true and a file is not audio | { "code": "VOICE_NOTE_NOT_AUDIO", "error": "..." } |
400 | voiceNote: true with more than one recording | { "code": "VOICE_NOTE_INVALID", "error": "..." } |
400 | voiceNote: true on a line that can’t send native voice notes | { "code": "VOICE_NOTE_NO_PRIVATE_API", "error": "..." } |
401 | Missing or invalid API key | { "error": "Unauthorized" } |
404 | No group with this id in your workspace | { "error": "Group conversation not found" } |
409 | The line in this group is no longer in your workspace | { "error": "The line in this group is no longer in your workspace" } |
502 | Your line rejected the send | { "success": false, "error": "...", "messageId": "..." } |
502 | The text went out but a file failed | { "success": false, "error": "Text sent, but 1 attachment(s) failed — ...", "messageId": "..." } |
503 | The voice note could not be queued | { "success": false, "error": "Could not queue the voice note. Try again." } |
504 | Your line did not answer in time | { "success": false, "error": "...", "messageId": "..." } |
On
502 or 504, part of the message may already be in the group. Read the
thread (below) before you resend, or everyone in the group gets it twice.
If a timed-out message does arrive, Tuco flips it to sent on its own.Read the group thread
Pass the group id asconversation. URL-encode it. The id contains ; and +,
and an unencoded + turns into a space, so the group is silently not found.
curl -G "https://app.tuco.ai/api/unibox/conversations" \
-H "Authorization: Bearer tuco_xxxxxxxxxxxxx" \
--data-urlencode "conversation=group:iMessage;+;chat606535035660201399"
conversations if it is on the page you asked for. Otherwise it
comes back as preselectedConversation. Each received message has
groupSenderAddress (the member who sent it).
{
"_id": "6aecdec997d47c708109eb9c",
"direction": "received",
"message": "Cool video came",
"isGroupChat": true,
"groupChatGuid": "iMessage;+;chat606535035660201399",
"groupSenderAddress": "sam@example.com"
}
What else works in a group
| Action | Works? | How |
|---|---|---|
| React (tapback) | Yes | Send Reaction with the group message’s _id |
| Typing indicator | No | Returns 400 for a group message |
| Webhooks | Yes | Subscribe to group.message.*: received, sent, failed, reaction |
| Create a group, add or remove members | No | Someone in the group does this from their phone |