Skip to main content

Overview

Every call to POST /api/messages creates a message document in Tuco. This page describes the shape of that document, the lifecycle it moves through, and how delivery works under the hood.
Messages sent via API appear in Unibox alongside manually sent and incoming messages. Same recipient = same thread.

Message object

_id
string
Unique message ID (MongoDB ObjectId)
message
string
Body text
messageType
string
"imessage" | "sms" | "email". Defaults to "imessage" when omitted from the API.
status
string
Current lifecycle state. See Status lifecycle below.
fromLineId
string
Line used to send
fromLinePhone
string
Denormalized phone from the line
fromLineEmail
string
Denormalized email from the line
recipientPhone
string
Recipient phone number
recipientEmail
string
Recipient email address
recipientName
string
Display name
leadId
string
Linked lead (if any)
workspaceId
string
Clerk organization ID
createdByUserId
string
Clerk user ID who created the message
scheduledDate
string
ISO 8601 timestamp — when to send
batchId
string
Grouping identifier
stepIndex
number
Campaign step index (0 = step 1)
settings
object
Time window settings:
sentAt
string
When actually sent
deliveredAt
string
When delivery confirmed via device callback
readAt
string
When read (incoming messages only)
externalMessageId
string
BlueBubbles / provider ID
errorMessage
string
Error details when failed or fallback
checkedAddresses
string[]
Addresses checked for iMessage availability (on fallback)
sendFallbackSmsOnFailed
boolean
When true + message fails, Tuco sends fallback SMS if Twilio is configured. Default: false.
campaignId
string
Campaign reference
hsContactId
string
HubSpot contact ID
hsPortalId
string
HubSpot portal ID
ghlContactId
string
GHL contact ID
ghlLocationId
string
GHL location ID
ghlConversationMessageId
string
GHL conversation message ID
createdAt
string
Creation timestamp
updatedAt
string
Last update timestamp

Status lifecycle

queued → pending → sending → sent → delivered
                               ↘ failed
                               ↘ fallback
                          ↗ scheduled (rescheduled)
                     cancelled
StatusMeaning
queuedNot yet picked for send (waiting for line limits, time window, device gap, or contact gap)
pendingAssigned to a line, worker is processing
sendingWorker is actively sending (prevents duplicate sends)
sentAccepted by the provider (BlueBubbles / Twilio)
deliveredDelivery confirmed via device callback (can come later)
failedTechnical send failure after all retries, or availability API error
fallbackAvailability check determined recipient has no iMessage
scheduledWill send at scheduledDate, or rescheduled due to device gap
cancelledUser or system cancelled
sent vs delivered: sent means the provider accepted the message. delivered means the recipient’s device confirmed receipt — this can arrive seconds or minutes later via a device callback.

Pre-send checks (why a message stays queued)

These checks run before a message is sent. None of them cause an API error — the message is accepted and waits.
CheckWhat it doesIf it fails
Time windowOnly send inside sendWindowStartsendWindowEnd on allowedDaysOfWeekStay queued until next valid time
Line limitsDaily total messages / daily new conversations per lineStay queued until limits reset
Device gapMinimum time between sends from the same physical device (default 30s)Queued or rescheduled
Contact gapMinimum time between first messages to different contacts on the same line (default 45s)Stay queued
You can check why a message is still queued by looking at your Loki/Grafana logs for events like individual.message.validation_failed, message.queued_limit, individual.message.device_gap_not_met, or individual.message.gap_not_met.

Retries

ContextSend attemptsVerification between attemptsGapEnv vars
Individual (API / Unibox)5outgoing_messages + BlueBubbles message/query (poll up to 90s)60sUNIBOX_REPLY_RETRY_*
App sync fallback3Same as individual30sSYNC_FALLBACK_RETRY_*
Campaign1 per line; step 0 tries other lines
The “app sync fallback” path only runs when the BullMQ queue is unreachable (e.g. Redis down). In normal operation, the worker handles all sends.

Replying via API

There is no special “reply” endpoint. To reply to a conversation, send another message to the same recipient using POST /api/messages. Unibox groups messages by recipient, so the new message appears in the existing thread.

Where messages appear

SurfaceWhat you see
UniboxPer-message status icon + label. All statuses except queued, scheduled, and cancelled.
Campaign detailRecipient-level status badges (Pending, Sent, Delivered, Failed, Fallback).
APIGET /api/messages with filters by status, line, lead, etc.