Send Drip
curl --request POST \
--url https://app.tuco.ai/api/drip \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"message": "<string>",
"recipientPhone": "<string>",
"recipientEmail": "<string>",
"messageType": "<string>",
"attachmentUrls": [
"<string>"
],
"voiceNote": true,
"fromLineId": "<string>",
"correlationId": "<string>",
"timezone": "<string>",
"sendWindowStart": "<string>",
"sendWindowEnd": "<string>",
"allowedDaysOfWeek": [
123
],
"hsContactId": "<string>",
"hsPortalId": "<string>",
"ghlContactId": "<string>",
"ghlLocationId": "<string>"
}
'import requests
url = "https://app.tuco.ai/api/drip"
payload = {
"message": "<string>",
"recipientPhone": "<string>",
"recipientEmail": "<string>",
"messageType": "<string>",
"attachmentUrls": ["<string>"],
"voiceNote": True,
"fromLineId": "<string>",
"correlationId": "<string>",
"timezone": "<string>",
"sendWindowStart": "<string>",
"sendWindowEnd": "<string>",
"allowedDaysOfWeek": [123],
"hsContactId": "<string>",
"hsPortalId": "<string>",
"ghlContactId": "<string>",
"ghlLocationId": "<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({
message: '<string>',
recipientPhone: '<string>',
recipientEmail: '<string>',
messageType: '<string>',
attachmentUrls: ['<string>'],
voiceNote: true,
fromLineId: '<string>',
correlationId: '<string>',
timezone: '<string>',
sendWindowStart: '<string>',
sendWindowEnd: '<string>',
allowedDaysOfWeek: [123],
hsContactId: '<string>',
hsPortalId: '<string>',
ghlContactId: '<string>',
ghlLocationId: '<string>'
})
};
fetch('https://app.tuco.ai/api/drip', 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/drip",
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([
'message' => '<string>',
'recipientPhone' => '<string>',
'recipientEmail' => '<string>',
'messageType' => '<string>',
'attachmentUrls' => [
'<string>'
],
'voiceNote' => true,
'fromLineId' => '<string>',
'correlationId' => '<string>',
'timezone' => '<string>',
'sendWindowStart' => '<string>',
'sendWindowEnd' => '<string>',
'allowedDaysOfWeek' => [
123
],
'hsContactId' => '<string>',
'hsPortalId' => '<string>',
'ghlContactId' => '<string>',
'ghlLocationId' => '<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/drip"
payload := strings.NewReader("{\n \"message\": \"<string>\",\n \"recipientPhone\": \"<string>\",\n \"recipientEmail\": \"<string>\",\n \"messageType\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"voiceNote\": true,\n \"fromLineId\": \"<string>\",\n \"correlationId\": \"<string>\",\n \"timezone\": \"<string>\",\n \"sendWindowStart\": \"<string>\",\n \"sendWindowEnd\": \"<string>\",\n \"allowedDaysOfWeek\": [\n 123\n ],\n \"hsContactId\": \"<string>\",\n \"hsPortalId\": \"<string>\",\n \"ghlContactId\": \"<string>\",\n \"ghlLocationId\": \"<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/drip")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"<string>\",\n \"recipientPhone\": \"<string>\",\n \"recipientEmail\": \"<string>\",\n \"messageType\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"voiceNote\": true,\n \"fromLineId\": \"<string>\",\n \"correlationId\": \"<string>\",\n \"timezone\": \"<string>\",\n \"sendWindowStart\": \"<string>\",\n \"sendWindowEnd\": \"<string>\",\n \"allowedDaysOfWeek\": [\n 123\n ],\n \"hsContactId\": \"<string>\",\n \"hsPortalId\": \"<string>\",\n \"ghlContactId\": \"<string>\",\n \"ghlLocationId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/drip")
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 \"message\": \"<string>\",\n \"recipientPhone\": \"<string>\",\n \"recipientEmail\": \"<string>\",\n \"messageType\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"voiceNote\": true,\n \"fromLineId\": \"<string>\",\n \"correlationId\": \"<string>\",\n \"timezone\": \"<string>\",\n \"sendWindowStart\": \"<string>\",\n \"sendWindowEnd\": \"<string>\",\n \"allowedDaysOfWeek\": [\n 123\n ],\n \"hsContactId\": \"<string>\",\n \"hsPortalId\": \"<string>\",\n \"ghlContactId\": \"<string>\",\n \"ghlLocationId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyMessages
Send Drip
Same body as POST /api/messages, built for bulk/bursty CRM enrollment fan-out — never 429s, fast-acks 202, drips out safely per line. Bearer-token auth, JSON.
POST
/
api
/
drip
Send Drip
curl --request POST \
--url https://app.tuco.ai/api/drip \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"message": "<string>",
"recipientPhone": "<string>",
"recipientEmail": "<string>",
"messageType": "<string>",
"attachmentUrls": [
"<string>"
],
"voiceNote": true,
"fromLineId": "<string>",
"correlationId": "<string>",
"timezone": "<string>",
"sendWindowStart": "<string>",
"sendWindowEnd": "<string>",
"allowedDaysOfWeek": [
123
],
"hsContactId": "<string>",
"hsPortalId": "<string>",
"ghlContactId": "<string>",
"ghlLocationId": "<string>"
}
'import requests
url = "https://app.tuco.ai/api/drip"
payload = {
"message": "<string>",
"recipientPhone": "<string>",
"recipientEmail": "<string>",
"messageType": "<string>",
"attachmentUrls": ["<string>"],
"voiceNote": True,
"fromLineId": "<string>",
"correlationId": "<string>",
"timezone": "<string>",
"sendWindowStart": "<string>",
"sendWindowEnd": "<string>",
"allowedDaysOfWeek": [123],
"hsContactId": "<string>",
"hsPortalId": "<string>",
"ghlContactId": "<string>",
"ghlLocationId": "<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({
message: '<string>',
recipientPhone: '<string>',
recipientEmail: '<string>',
messageType: '<string>',
attachmentUrls: ['<string>'],
voiceNote: true,
fromLineId: '<string>',
correlationId: '<string>',
timezone: '<string>',
sendWindowStart: '<string>',
sendWindowEnd: '<string>',
allowedDaysOfWeek: [123],
hsContactId: '<string>',
hsPortalId: '<string>',
ghlContactId: '<string>',
ghlLocationId: '<string>'
})
};
fetch('https://app.tuco.ai/api/drip', 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/drip",
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([
'message' => '<string>',
'recipientPhone' => '<string>',
'recipientEmail' => '<string>',
'messageType' => '<string>',
'attachmentUrls' => [
'<string>'
],
'voiceNote' => true,
'fromLineId' => '<string>',
'correlationId' => '<string>',
'timezone' => '<string>',
'sendWindowStart' => '<string>',
'sendWindowEnd' => '<string>',
'allowedDaysOfWeek' => [
123
],
'hsContactId' => '<string>',
'hsPortalId' => '<string>',
'ghlContactId' => '<string>',
'ghlLocationId' => '<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/drip"
payload := strings.NewReader("{\n \"message\": \"<string>\",\n \"recipientPhone\": \"<string>\",\n \"recipientEmail\": \"<string>\",\n \"messageType\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"voiceNote\": true,\n \"fromLineId\": \"<string>\",\n \"correlationId\": \"<string>\",\n \"timezone\": \"<string>\",\n \"sendWindowStart\": \"<string>\",\n \"sendWindowEnd\": \"<string>\",\n \"allowedDaysOfWeek\": [\n 123\n ],\n \"hsContactId\": \"<string>\",\n \"hsPortalId\": \"<string>\",\n \"ghlContactId\": \"<string>\",\n \"ghlLocationId\": \"<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/drip")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"<string>\",\n \"recipientPhone\": \"<string>\",\n \"recipientEmail\": \"<string>\",\n \"messageType\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"voiceNote\": true,\n \"fromLineId\": \"<string>\",\n \"correlationId\": \"<string>\",\n \"timezone\": \"<string>\",\n \"sendWindowStart\": \"<string>\",\n \"sendWindowEnd\": \"<string>\",\n \"allowedDaysOfWeek\": [\n 123\n ],\n \"hsContactId\": \"<string>\",\n \"hsPortalId\": \"<string>\",\n \"ghlContactId\": \"<string>\",\n \"ghlLocationId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/drip")
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 \"message\": \"<string>\",\n \"recipientPhone\": \"<string>\",\n \"recipientEmail\": \"<string>\",\n \"messageType\": \"<string>\",\n \"attachmentUrls\": [\n \"<string>\"\n ],\n \"voiceNote\": true,\n \"fromLineId\": \"<string>\",\n \"correlationId\": \"<string>\",\n \"timezone\": \"<string>\",\n \"sendWindowStart\": \"<string>\",\n \"sendWindowEnd\": \"<string>\",\n \"allowedDaysOfWeek\": [\n 123\n ],\n \"hsContactId\": \"<string>\",\n \"hsPortalId\": \"<string>\",\n \"ghlContactId\": \"<string>\",\n \"ghlLocationId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyThe Try it panel runs against production (
https://app.tuco.ai) with the key you paste
into it. There is no sandbox — a request from this page queues a real message from one of your lines.Use this instead of
POST /api/messages
for bulk sends (HubSpot/CRM parallel enrollment, list blasts). Same request
body — change only the URL. It never 429s a blast, acks 202 immediately, and
drips the sends out safely per line. Full guide: Drip.Authentication
Pass your workspace API key as a Bearer token (same key as/api/messages).
Authorization: Bearer tuco_xxxxxxxxxxxxx
Request body
Identical toPOST /api/messages — the fields below are
the ones a bulk enrolment actually uses. Everything else that endpoint accepts works here too.
string
The text to send. Required unless
attachmentUrls is provided.Send the finished text — merge fields are not substituted here. A body still containing
{{firstName}} is rejected with 400 MALFORMED_NAME_TEMPLATE.string
The phone number to text, in E.164 format (e.g.
"+14155551234"). This is how you address a
drip send. Required unless you pass recipientEmail.string
Only for
messageType: "email", or an Apple ID you already know is reachable. Prefer
recipientPhone. Required unless you pass recipientPhone.string
default:"imessage"
imessage, sms or email. Anything else returns
400 Invalid messageType. Must be email, sms, or imessage.string[]
Public URLs to attach. Only
http/https entries are kept. When present, message may be empty.boolean
default:"false"
Deliver a single audio attachment as a native iMessage voice note.
string
Pin the sending line. Omit to round-robin across the workspace’s active lines.
string
Your own id for this send, echoed back in the
202 and in every webhook. Also accepted as the
x-correlation-id header, which wins. Omit and Tuco generates one (drip_…).string
IANA timezone for the send window, e.g.
"America/New_York". Required if you set a window.string
Earliest send time,
HH:mm. Must be HH:mm or the request returns 400 Invalid time format.string
Latest send time,
HH:mm.number[]
Days sending is allowed.
0 = Sunday, 6 = Saturday.string
HubSpot contact id, for linking the send back to the CRM record.
string
HubSpot portal id.
string
GoHighLevel contact id.
string
GoHighLevel location id.
Example
curl -X POST "https://app.tuco.ai/api/drip" \
-H "Authorization: Bearer tuco_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: hs-enroll-8471-step2" \
-d '{
"message": "Hi Jane — following up on your quote.",
"recipientPhone": "+14155551234",
"hsContactId": "8471",
"hsPortalId": "1234567"
}'
Success (202 Accepted)
{
"status": "queued",
"correlationId": "drip_1791225101362_7vvdij",
"scheduledDate": "2026-10-05T18:31:44.089Z",
"jitterMs": 2727
}
Errors
| Status | When | Body |
|---|---|---|
400 | message and attachmentUrls both empty | { "error": "Message and attachmentUrls cannot both be empty" } |
400 | No recipientPhone or recipientEmail | { "error": "Either recipientEmail or recipientPhone is required." } |
400 | recipientPhone is not a valid number | { "error": "Invalid recipientPhone format..." } |
400 | Invalid messageType | { "error": "Invalid messageType. Must be email, sms, or imessage" } |
400 | message still contains a merge field | { "error": "Malformed message...", "code": "MALFORMED_NAME_TEMPLATE" } |
400 | Bad time window or timezone | { "error": "Invalid time format..." } |
400 | Body is not a JSON object | { "error": "Request body must be a JSON object" } |
401 | Missing or invalid API key | { "error": "Unauthorized" } |
How it differs from /api/messages
- Not rate-limited — exempt from the 60/min send limit; a bulk blast is never rejected.
- Always
202— the request is buffered and the message is created a moment later, then sent still later (paced per line by the ban protector: ~7 new/hr, ~50 new/day, 150 total/day). - Response is
202 { status, correlationId, scheduledDate, jitterMs }— nomessageId(the message doesn’t exist yet at ack time); usecorrelationIdto correlate in your webhooks. Idempotency-Keyheader is honored (HubSpot retries return the first ack).
For low-volume, send-now traffic where you want the real delivery status back
immediately, use
POST /api/messages
instead.