Check Replies
curl --request GET \
--url https://app.tuco.ai/api/replies \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/replies"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.tuco.ai/api/replies', 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/replies",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/replies"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.tuco.ai/api/replies")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/replies")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"leadId": "<string>",
"messageId": "<string>",
"message": "<string>",
"respondedAtUtc": "<string>",
"recipientPhone": "<string>",
"recipientEmail": "<string>",
"name": "<string>",
"hasReplied": true,
"hasEverReplied": true,
"attachmentDownloadUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
],
"recentReplies": [
{
"messageId": "<string>",
"message": "<string>",
"receivedAtUtc": "<string>",
"attachmentDownloadUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
]
}
],
"parentMessages": [
{
"messageId": "<string>",
"message": "<string>",
"sentAtUtc": "<string>",
"batchId": "<string>",
"stepIndex": 123
}
]
}Replies
Check Replies
Get replies for a lead or campaign — who replied, what they said, and the conversation context. REST endpoint in the Tuco AI iMessage API — bearer-token auth.
GET
/
api
/
replies
Check Replies
curl --request GET \
--url https://app.tuco.ai/api/replies \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.tuco.ai/api/replies"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.tuco.ai/api/replies', 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/replies",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/replies"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.tuco.ai/api/replies")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/replies")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"leadId": "<string>",
"messageId": "<string>",
"message": "<string>",
"respondedAtUtc": "<string>",
"recipientPhone": "<string>",
"recipientEmail": "<string>",
"name": "<string>",
"hasReplied": true,
"hasEverReplied": true,
"attachmentDownloadUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
],
"recentReplies": [
{
"messageId": "<string>",
"message": "<string>",
"receivedAtUtc": "<string>",
"attachmentDownloadUrls": [
"<string>"
],
"attachmentNames": [
"<string>"
]
}
],
"parentMessages": [
{
"messageId": "<string>",
"message": "<string>",
"sentAtUtc": "<string>",
"batchId": "<string>",
"stepIndex": 123
}
]
}Use this endpoint to check if a specific lead has replied, or to list all leads who replied (optionally scoped to a campaign). Returns the reply text, up to 10 recent replies from the lead, and up to 10 outbound messages you sent them.When a lead replies with a photo or file, the row (and each
recentReplies entry) carries attachmentDownloadUrls — public, signed URLs you can GET directly to download the image/file bytes, no auth header needed. A reply can include many attachments; for a large photo burst the list fills in over a minute or two as background ingestion completes, so poll again if you need the complete set. See Downloading inbound attachments.Authentication
Authorization: Bearer tuco_sk_xxxxxxxxxxxxx
Query parameters
string
Replies from this lead only (valid ObjectId; returns 404 if lead not found).
string
Restrict to leads who replied and are in this campaign.
string
Filter by reply sender email.
string
Filter by reply sender phone (E.164 or normalized).
string
Restrict to replies received on this Tuco line (24-char ObjectId hex). Returns
400 if malformed.string
Inclusive ISO 8601 UTC lower bound on the reply timestamp (e.g.
2026-06-01T00:00:00Z). Returns 400 if unparseable or greater than dateTo.string
Inclusive ISO 8601 UTC upper bound on the reply timestamp.
number
default:"1"
Page number.
number
default:"50"
Results per page (max 100).
Examples
curl "https://app.tuco.ai/api/replies?recipientPhone=%2B14422646782&limit=10" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
curl "https://app.tuco.ai/api/replies?leadId=69b2c41d352a78479d2c623b" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
curl "https://app.tuco.ai/api/replies?campaignId=69ab444ac1feb77d7f46462f&page=1&limit=50" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
curl "https://app.tuco.ai/api/replies?recipientEmail=john%40example.com" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
curl "https://app.tuco.ai/api/replies?lineId=60d5ec49e3a8a0001f9b9999&dateFrom=2026-06-01T00:00:00Z&dateTo=2026-06-10T23:59:59Z&page=1&limit=100" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
Success response (200)
{
"replies": [
{
"leadId": "69b2c41d352a78479d2c623b",
"messageId": "69b340dd6dae1d942ece4d09",
"message": "Stop",
"respondedAtUtc": "2026-03-12T22:40:29.174Z",
"recipientPhone": "+919876543210",
"recipientEmail": "jane@example.com",
"name": "Jane Doe",
"hasReplied": false,
"hasEverReplied": true,
"attachmentDownloadUrls": [
"https://app.tuco.ai/api/attachments/crm-media/IMG_1380.jpeg?u=aHR0cHM...&exp=1786752000000&sig=Vh9k..."
],
"attachmentNames": ["IMG_1380.jpeg"],
"recentReplies": [
{
"messageId": "69b340dd6dae1d942ece4d09",
"message": "Stop",
"receivedAtUtc": "2026-03-12T22:40:29.174Z"
},
{
"messageId": "69b33fdd6dae1d942ece4d08",
"message": "Here's the photo you asked for",
"receivedAtUtc": "2026-03-12T21:15:00.000Z",
"attachmentDownloadUrls": [
"https://app.tuco.ai/api/attachments/crm-media/IMG_1380.jpeg?u=aHR0cHM...&exp=1786752000000&sig=Vh9k..."
],
"attachmentNames": ["IMG_1380.jpeg"]
}
],
"parentMessages": [
{
"messageId": "69b344bc6dae1d942ece4d0e",
"message": "Here's your document.",
"sentAtUtc": "2026-03-12T22:57:02.892Z",
"batchId": null,
"stepIndex": null
}
]
}
],
"total": 1,
"page": 1,
"limit": 50,
"totalPages": 1
}
Reply fields
string
Lead who replied.
string
ID of the most recent reply message.
string
The reply text (e.g. “Stop”, “Interested!”, “Call me tomorrow”).
string
ISO 8601 timestamp of the most recent reply.
string
Lead’s phone number.
string
Lead’s email address.
string
Lead’s display name.
boolean
true when the lead’s newest inbound is newer than your newest outbound to them (no outbound on record → true). Any follow-up you send after their reply — including AI copilot auto-replies — flips it back to false. With campaignId set, outbounds are scoped to that campaign, so this reads as “replied since the campaign’s last message”.boolean
Always
true for a returned row — leads who never replied have no row. Included so this endpoint and the GHL Check for Replies action expose the same field names; branch on it when an auto-responder makes hasReplied flip to false within seconds.string[]
Present only when the most recent reply included attachments. Public, signed, 30-day URLs —
GET each one (no auth header) to download the actual image/file bytes, served with the correct Content-Type. Omitted for text-only replies. For a large photo burst this array may grow for a minute or two after the reply as background ingestion finishes — poll again to get the complete set. See Downloading inbound attachments.string[]
Filenames aligned 1:1 with
attachmentDownloadUrls (e.g. ["IMG_1380.jpeg"]). Omitted for text-only replies.recentReplies (up to 10)
All inbound replies from this lead, newest first. Gives you the full conversation context from their side.array
Show Reply object
Show Reply object
string
Reply message ID.
string
Reply text (truncated to 200 chars).
string
When the reply was received (UTC ISO 8601).
string[]
Present only when this reply included attachments. Public, signed download URLs for the image/file bytes (omitted otherwise).
string[]
Filenames aligned 1:1 with
attachmentDownloadUrls.parentMessages (up to 10)
Outbound messages you sent to this lead, newest first. Shows what you said before they replied.array
Error responses
| Status | When | Body |
|---|---|---|
400 | Invalid leadId or campaignId | { "error": "Invalid leadId" } or { "error": "Invalid campaignId" } |
400 | Invalid lineId (not a 24-char ObjectId hex) | { "error": "Invalid lineId" } |
400 | dateFrom / dateTo unparseable | { "error": "Invalid dateFrom" } or { "error": "Invalid dateTo" } |
400 | dateFrom > dateTo | { "error": "dateFrom must be <= dateTo" } |
401 | Missing or invalid API key | { "error": "Unauthorized" } |
404 | leadId provided but lead not found | { "error": "Lead not found or access denied" } |
404 | campaignId provided but campaign not found | { "error": "Campaign not found" } |
429 | Rate-limited (120 req/min per workspace) | { "error": "Rate limit exceeded" } |
500 | Server error | { "error": "Failed to fetch replies" } |
Downloading inbound attachments
When a lead replies with a photo or file, Tuco stores the bytes in private blob storage. The raw storage URL is not directly fetchable — it returns403. Instead, every attachment-bearing reply exposes attachmentDownloadUrls: signed, 30-day proxy URLs that stream the real bytes with the correct Content-Type.
You can fetch a download URL with either credential — pick whichever fits:
Just the URL (signature)
The URL already carries an HMAC signature (
&sig=…), so no header is needed. Drop it into an <img src>, a re-hosting job, or curl -O. This is the simplest path. The signature is unforgeable and expires after 30 days.Your API key
Prefer to pass a token? Send
Authorization: Bearer tuco_sk_…. Tuco verifies the key’s workspace owns that attachment, so a key can only ever read its own workspace’s files. Revocable by rotating the key.The signed URL grants read access to that one file for 30 days. Treat it like a capability: fetch and store the bytes in your own system rather than passing the URL around long-term. To fetch a fresh signed URL, just call
GET /api/replies (or listen for the message.reply webhook) again.Download every photo a lead sent
# 1) List the lead's replies and pull the download URLs + filenames
curl -s "https://app.tuco.ai/api/replies?leadId=69b2c41d352a78479d2c623b" \
-H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
| jq -r '.replies[].recentReplies[]
| select(.attachmentDownloadUrls != null)
| .attachmentNames[] as $n | .attachmentDownloadUrls[] | [$n, .] | @tsv' \
| while IFS=$'\t' read -r name url; do
# 2) Fetch the bytes — the signature in the URL is the credential, no header needed.
# (Prefer your own token? add: -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx")
curl -s "$url" -o "$name"
echo "saved $name"
done
attachmentDownloadUrls / attachmentNames fields are delivered in real time on the message.reply webhook and appear on inbound messages from GET /api/messages, so you can ingest photos push-style instead of polling.