Skip to main content
POST
/
api
/
messages
Send Message
curl --request POST \
  --url https://app.tuco.ai/api/messages \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "message": "<string>",
  "attachmentUrls": [
    "<string>"
  ],
  "recipientPhone": "<string>",
  "recipientEmail": "<string>",
  "messageType": "<string>",
  "fromLineId": "<string>",
  "leadId": "<string>",
  "recipientName": "<string>"
}
'
{
  "success": true,
  "status": "<string>",
  "message": {
    "_id": "<string>",
    "message": "<string>",
    "messageType": "<string>",
    "status": "<string>",
    "fromLineId": "<string>",
    "recipientPhone": "<string>",
    "recipientEmail": "<string>",
    "leadId": "<string>",
    "scheduledDate": "<string>",
    "sentAt": "<string>",
    "deliveredAt": "<string>",
    "createdAt": "<string>"
  }
}
This is the primary endpoint for automation tools like GetSales, n8n, Make, or your own backend. One call = one message. Tuco handles retries, delivery verification, and fallback internally.

Authentication

Pass your workspace API key as a Bearer token, or use a Clerk session token.
Authorization: Bearer tuco_sk_xxxxxxxxxxxxx

Request body

message
string
The text content to send. Required unless attachmentUrls is provided (you can send attachments only, or message + attachments). Personalization placeholders like {{firstName}} are resolved from the lead at send time.
attachmentUrls
string[]
Optional array of URLs (max 2, max 25 MB each). When provided, message can be empty. Supported formats: images (PNG, JPG, GIF, WebP, HEIC), video (MP4, MOV), audio (CAF), and PDF. Supported sources: any public URL, UploadThing/Blob URLs that belong to your workspace.
Sending the same attachment URL repeatedly (e.g. a promo video in an automation)? Tuco caches attachments per line — the second send skips the download and upload, making it significantly faster.
recipientPhone
string
Phone number in E.164 format (e.g. "+12025551234"). Required unless recipientEmail or leadId is provided.
recipientEmail
string
Email address for email or iMessage (Apple ID). Required unless recipientPhone or leadId is provided.
messageType
string
default:"imessage"
Channel to use. When omitted, defaults to "imessage".
ValueChannelAddresses tried (from lead)
"imessage"iMessage via device relayphone → altPhone1-3 → email → altEmail1-3
"sms"SMS via Twiliophone → altPhone1-3 only
"email"Emailemail → altEmail1-3 only
fromLineId
string
Tuco line ID to send from. Optional — when omitted, Tuco round-robins across your workspace’s active lines automatically.
leadId
string
Send to an existing lead using their stored contact details. When provided together with recipientPhone/recipientEmail, the body recipient is used as the send-to address while the lead is used for linking.
recipientName
string
Display name for the recipient. Derived from the lead when omitted.
Stored on the lead (created or found). The worker tries these in priority order when the primary address fails iMessage availability.
altPhone1
string
Alternate phone number 1
altPhone2
string
Alternate phone number 2
altPhone3
string
Alternate phone number 3
altEmail1
string
Alternate email 1
altEmail2
string
Alternate email 2
altEmail3
string
Alternate email 3
Control when the message is sent. All fields are optional. When omitted, the message sends immediately (subject to line limits and device gaps).
scheduledDate
string
ISO 8601 timestamp to send in the future (e.g. "2025-10-15T14:30:00Z").
timezone
string
IANA timezone for the send window (e.g. "America/New_York"). Required if using sendWindowStart/sendWindowEnd.
sendWindowStart
string
Earliest time to send, HH:mm format (e.g. "09:00").
sendWindowEnd
string
Latest time to send, HH:mm format (e.g. "17:00").
allowedDaysOfWeek
number[]
Days when sending is allowed. 0 = Sunday, 6 = Saturday. Example: [1,2,3,4,5] for weekdays only.
sendFallbackSmsOnFailed
boolean
default:"false"
When true, if the message ends in failed status (technical error after retries), Tuco sends a fallback SMS via your configured Twilio number. Returns 400 with code: "FALLBACK_NOT_CONFIGURED" if enabled but no fallback is set up on your workspace.
forceFallback
boolean
default:"false"
When true, Tuco skips the iMessage availability check entirely and sends the message straight via your workspace’s configured fallback — Twilio, GHL, or a custom webhook (Settings → When iMessage isn’t available). Use it when you already know the recipient isn’t on iMessage, or you simply want SMS.The send never touches iMessage, so it does not consume the line’s daily cap or the availability-check budget, and the response status is "fallback".If no fallback is configured — or the fallback dispatch fails (e.g. a bad Twilio from-number) — the message is not delivered: the response returns "success": false with a channel-tagged error, and the message is recorded with "fallbackSmsStatus": "failed". (This is distinct from the sendFallbackSmsOnFailed 400 — forceFallback always returns 200 with the success flag in the body.)Unlike sendFallbackSmsOnFailed (which is reactive — fallback only after an iMessage send fails), forceFallback is proactive: it never attempts iMessage in the first place.
batchId
string
Free-form string to group related messages for reporting.
correlationId
string
Optional tracing ID. If omitted, Tuco generates one (app_…). The same ID is preferred from the x-correlation-id request header. It flows through every Loki event for this send so you can grep one ID and see the entire request → gate → send → webhook chain. See API Overview → Correlation IDs.
Headers Tuco recognizes in addition to Authorization.
HeaderPurpose
x-correlation-idRequest-scoped tracing ID (preferred over body field). Echoed back in Loki under correlationId.
x-execution-idGHL workflow execution ID. Only set when calling from a GHL workflow context.
x-ghl-workflow-idGHL workflow definition ID. Pairs with x-execution-id.
Idempotency-KeyWhen supplied, Tuco caches the response and replays it on retry within the idempotency window. Use one unique key per logical send.

Examples

Create or import a lead first using POST /api/leads, then send using leadId as shown below.
curl -X POST "https://app.tuco.ai/api/messages" \
  -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Hi, this is a test from Tuco.",
    "leadId": "667f1f77bcf86cd799439012"
  }'

Response

The API always returns 201 when the message is successfully created. The status field tells you what happens next.
Pre-send checks are never errors. Line limits, time windows, device gaps, and contact gaps cause status: "pending" or "scheduled" — the message is accepted and will send when conditions are met.

Success (201 Created or 200 OK when duplicate lead used)

{
  "success": true,
  "status": "sent",
  "message": {
    "_id": "69b344bc6dae1d942ece4d0e",
    "message": "Here's your document.",
    "messageType": "imessage",
    "status": "sent",
    "fromLineId": "69ab427ac1feb77d7f46462e",
    "recipientPhone": "+919042956129",
    "recipientEmail": "goforbg@icloud.com",
    "recipientName": "BG Real",
    "leadId": "69b2c41d352a78479d2c623b",
    "workspaceId": "org_3AZs4H8UsfFxVFONr6H4K75okaG",
    "createdAt": "2026-03-12T22:57:00.856Z",
    "updatedAt": "2026-03-12T22:57:02.892Z",
    "sentAt": "2026-03-12T22:57:02.892Z",
    "externalMessageId": "5CA93911-F0FE-4D1F-8D8C-1E3495A124F6"
  },
  "leadId": "69b2c41d352a78479d2c623b",
  "ghlContactId": "aMUQn0u0Z7cw0NQ7tJ5R",
  "ghlLocationId": "eL7DD22BdZ0rismu7qCA",
  "hsPortalId": null,
  "hsContactId": null
}
When the recipient was matched to an existing lead (duplicate), the API returns 200 with duplicateOnly: true, existingLeadIds, and leadIds so your automation can continue without treating it as an error.
success
boolean
true when the message was created.
status
string
Current status of the message. See the status table below.
message
object
The full message document. Key fields:

Status values

StatusMeaningIs it an error?
"sent"Message sent immediately (sync path)No
"pending"Created, worker will process. Line limits / time window / device gap may delay it.No — it will send when checks pass
"scheduled"Will send at scheduledDate, or rescheduled due to device gapNo
"fallback"Recipient has no iMessage; fallback SMS sent if configuredNo (business rule)
"failed"All retries exhausted or availability API errorYes (technical)
"pending" and "scheduled" mean the message is accepted and queued. The worker sends it when:
  • Time window is satisfied (inside sendWindowStartsendWindowEnd)
  • Allowed day is satisfied (today is in allowedDaysOfWeek)
  • Line limits reset (daily total or new conversations limit)
  • Device gap is met (default 30s between sends from same device)
  • Contact gap is met (default 45s between first messages to different contacts on same line)
None of these cause an error response.

Duplicate protection

Tuco automatically blocks duplicate sends — no header required. Two POSTs with identical (workspaceId, recipientPhone, recipientEmail, message) within 15 minutes are collapsed: the second one returns 200 with the original message and does not fire a new send.
HTTP 200 — duplicate caught
{
  "success": true,
  "duplicateOnly": true,
  "duplicateMessage": "Same message text already sent to this contact 42s ago — returning existing message instead of sending again",
  "message": { /* the original sibling message doc */ },
  "leadId": "..."
}
Branch on duplicateOnly === true and treat it as a no-op success. The message._id in the response is the original send, so your CRM workflow can keep moving without re-sending.
This works for concurrent firings (the most common cause of accidental duplicates — an integration retrying within milliseconds) AND for upstream retries up to 15 minutes apart. The guard is built on an atomic Mongo write, so concurrent identical POSTs cannot both proceed.
Failed sends: if your first send ended in status: "failed" and you immediately retry with the same body within 15 minutes, you’ll get the failed sibling back with duplicateOnly: true. To force a fresh send: change the body or wait 15 minutes.
For deeper detail, edge cases, and ops events, see Atomic Duplicate Guard. Idempotency-Key is still supported and runs before the dedup guard. Use it when your client controls retries — it gives you a 24-hour window keyed on the exact request rather than on body content.

Errors

Errors are returned only for validation failures — never for pre-send checks.
StatusWhenExample
400Message and attachments both empty{ "error": "Message and attachmentUrls cannot both be empty" }
400Invalid messageType{ "error": "Invalid messageType. Must be email, sms, or imessage" }
400No active lines + no fromLineId{ "error": "No active lines in workspace..." }
400No recipient anywhere{ "error": "Either recipientEmail or recipientPhone is required..." }
400sendFallbackSmsOnFailed: true but fallback not configured{ "error": "...", "code": "FALLBACK_NOT_CONFIGURED" }
401Invalid or missing API key{ "error": "Unauthorized" }
402Subscription past due (workspace read-only){ "error": "READ_ONLY", "code": "READ_ONLY", "reason": "past_due" }
404leadId provided but not found{ "error": "Lead not found or access denied" }

What happens after the API call

1

Message created

Tuco inserts a record in the messages collection with status pending (or scheduled if scheduledDate is provided).
2

Pre-send checks (worker)

The worker checks line limits, time window, device gap, and contact gap. If any fail, the message stays queued and is retried later — not failed.
3

Availability check

If the workspace has a line with Private API, Tuco checks whether the recipient supports iMessage. If not → status becomes fallback and fallback SMS fires (when configured).
forceFallback: true skips this step entirely — the message goes straight to the configured fallback channel and never runs an availability check (so it doesn’t consume the availability-check budget or line cap).
4

Send

For individual messages: up to 5 send attempts with delivery verification between each (polling for up to 90 seconds). For campaigns: 1 attempt per line.
5

Outcome

sent → appears in Unibox + webhook fires. failed → error recorded + optional fallback SMS. delivered → confirmed later via device callbacks.

Lead resolution

When you send without a leadId, Tuco resolves the recipient automatically:
If an existing lead in your workspace matches the recipientPhone or recipientEmail, the message is linked to that lead. Any altPhone/altEmail fields you pass will update the lead (merge, not overwrite).
If no matching lead exists, Tuco creates one under a list called “Quick Sends” (created automatically if it doesn’t exist). Alt contact fields are stored on the new lead.
The lead must exist in your workspace. The message is linked to it. recipientPhone/recipientEmail from the body override the lead’s stored contact for this specific send.

Alt contact fields

When you pass altPhone1altPhone3 or altEmail1altEmail3, they are stored on the lead (not the message). The worker uses them as fallback addresses when the primary address fails the iMessage availability check. Priority order for messageType: "imessage":
phone → altPhone1 → altPhone2 → altPhone3 → email → altEmail1 → altEmail2 → altEmail3
Alt fields are merged onto the lead. If a lead already has altPhone1 set and you send a new message without altPhone1, the existing value is preserved. Only explicitly provided fields are overwritten.

Fallback SMS on failure

By default, when a message fails (technical error), no SMS fallback is sent. To enable:
  1. Configure Twilio on your workspace (GET /api/workspace/fallback-config).
  2. Set sendFallbackSmsOnFailed: true in the request body (per-message) or on the workspace (applies to all messages).
If you set sendFallbackSmsOnFailed: true but fallback is not configured, the API returns 400 with code: "FALLBACK_NOT_CONFIGURED".
ScenarioFallback SMS fires?
Status = fallback (no iMessage), Twilio configuredYes — always
Status = failed, sendFallbackSmsOnFailed: true, Twilio configuredYes
Status = failed, sendFallbackSmsOnFailed: false (default)No
Status = failed, flag is true but no Twilio400 error at API call time
Fallback SMS requires recipientPhone on the message. Email-only messages will not trigger fallback SMS.