Overview
Every call toPOST /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
Unique message ID (MongoDB ObjectId)
Body text
"imessage" | "sms" | "email". Defaults to "imessage" when omitted from the API.Current lifecycle state. See Status lifecycle below.
Line used to send
Denormalized phone from the line
Denormalized email from the line
Recipient phone number
Recipient email address
Display name
Linked lead (if any)
Clerk organization ID
Clerk user ID who created the message
Scheduling & settings fields
Scheduling & settings fields
Delivery tracking fields
Delivery tracking fields
Error & fallback fields
Error & fallback fields
Integration fields
Integration fields
Creation timestamp
Last update timestamp
Status lifecycle
| Status | Meaning |
|---|---|
| queued | Not yet picked for send (waiting for line limits, time window, device gap, or contact gap) |
| pending | Assigned to a line, worker is processing |
| sending | Worker is actively sending (prevents duplicate sends) |
| sent | Accepted by the provider (BlueBubbles / Twilio) |
| delivered | Delivery confirmed via device callback (can come later) |
| failed | Technical send failure after all retries, or availability API error |
| fallback | Availability check determined recipient has no iMessage |
| scheduled | Will send at scheduledDate, or rescheduled due to device gap |
| cancelled | User or system cancelled |
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.| Check | What it does | If it fails |
|---|---|---|
| Time window | Only send inside sendWindowStart–sendWindowEnd on allowedDaysOfWeek | Stay queued until next valid time |
| Line limits | Daily total messages / daily new conversations per line | Stay queued until limits reset |
| Device gap | Minimum time between sends from the same physical device (default 30s) | Queued or rescheduled |
| Contact gap | Minimum 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
| Context | Send attempts | Verification between attempts | Gap | Env vars |
|---|---|---|---|---|
| Individual (API / Unibox) | 5 | outgoing_messages + BlueBubbles message/query (poll up to 90s) | 60s | UNIBOX_REPLY_RETRY_* |
| App sync fallback | 3 | Same as individual | 30s | SYNC_FALLBACK_RETRY_* |
| Campaign | 1 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 usingPOST /api/messages. Unibox groups
messages by recipient, so the new message appears in the existing thread.
Where messages appear
| Surface | What you see |
|---|---|
| Unibox | Per-message status icon + label. All statuses except queued, scheduled, and cancelled. |
| Campaign detail | Recipient-level status badges (Pending, Sent, Delivered, Failed, Fallback). |
| API | GET /api/messages with filters by status, line, lead, etc. |