> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuco.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Messages

> The message object, lifecycle, and how Tuco processes your sends. Part of the Tuco AI iMessage API reference — bearer-token auth, JSON responses, examples in.

## 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.

<Info>
  Messages sent via API appear in **Unibox** alongside manually sent and incoming
  messages. Same recipient = same thread.
</Info>

***

## Message object

<ResponseField name="_id" type="string">Unique message ID (MongoDB ObjectId)</ResponseField>
<ResponseField name="message" type="string">Body text</ResponseField>

<ResponseField name="messageType" type="string">
  `"imessage"` | `"sms"` | `"email"`. Defaults to `"imessage"` when omitted from the API.
</ResponseField>

<ResponseField name="status" type="string">
  Current lifecycle state. See [Status lifecycle](#status-lifecycle) below.
</ResponseField>

<ResponseField name="fromLineId" type="string">Line used to send</ResponseField>
<ResponseField name="fromLinePhone" type="string">Denormalized phone from the line</ResponseField>
<ResponseField name="fromLineEmail" type="string">Denormalized email from the line</ResponseField>
<ResponseField name="recipientPhone" type="string">Recipient phone number</ResponseField>
<ResponseField name="recipientEmail" type="string">Recipient email address</ResponseField>
<ResponseField name="recipientName" type="string">Display name</ResponseField>
<ResponseField name="leadId" type="string">Linked lead (if any)</ResponseField>
<ResponseField name="workspaceId" type="string">Clerk organization ID</ResponseField>
<ResponseField name="createdByUserId" type="string">Clerk user ID who created the message</ResponseField>
<ResponseField name="direction" type="string">`received` for inbound (a reply from the lead), `sent` for outbound. Inbound messages are where lead-sent photos live.</ResponseField>

<Accordion title="Attachment fields (photos & files)">
  <ResponseField name="attachmentDownloadUrls" type="string[]">
    Present only when the message has attachments. **Public, signed, 30-day URLs — `GET` each one (no auth header) to download the real image/file bytes** with the correct `Content-Type`. This is how you retrieve a photo a lead sent you: the raw `attachmentUrls` below point at private storage and return `403` on their own.
  </ResponseField>

  <ResponseField name="attachmentNames" type="string[]">Filenames aligned 1:1 with `attachmentDownloadUrls` (e.g. `["IMG_1380.jpeg"]`).</ResponseField>
  <ResponseField name="attachmentUrls" type="string[]">Raw storage URLs (private for inbound / Tuco-hosted). Kept for reference — use `attachmentDownloadUrls` to fetch bytes.</ResponseField>
</Accordion>

<Accordion title="Scheduling & settings fields">
  <ResponseField name="scheduledDate" type="string">ISO 8601 timestamp — when to send</ResponseField>
  <ResponseField name="batchId" type="string">Grouping identifier</ResponseField>
  <ResponseField name="stepIndex" type="number">Campaign step index (0 = step 1)</ResponseField>

  <ResponseField name="settings" type="object">
    Time window settings:

    <Expandable title="settings fields">
      <ResponseField name="timezone" type="string">IANA timezone</ResponseField>
      <ResponseField name="sendWindowStart" type="string">HH:mm</ResponseField>
      <ResponseField name="sendWindowEnd" type="string">HH:mm</ResponseField>
      <ResponseField name="allowedDaysOfWeek" type="number[]">0=Sun … 6=Sat</ResponseField>
    </Expandable>
  </ResponseField>
</Accordion>

<Accordion title="Delivery tracking fields">
  <ResponseField name="sentAt" type="string">When actually sent</ResponseField>
  <ResponseField name="deliveredAt" type="string">When delivery confirmed via device callback</ResponseField>
  <ResponseField name="readAt" type="string">When read (incoming messages only)</ResponseField>
  <ResponseField name="externalMessageId" type="string">device message ID</ResponseField>
</Accordion>

<Accordion title="Error & fallback fields">
  <ResponseField name="errorMessage" type="string">Error details when `failed` or `fallback`</ResponseField>
  <ResponseField name="checkedAddresses" type="string[]">Addresses checked for iMessage availability (on `fallback`)</ResponseField>

  <ResponseField name="sendFallbackSmsOnFailed" type="boolean">
    When `true` + message fails, Tuco sends fallback SMS if Twilio is configured.
    Default: `false`.
  </ResponseField>
</Accordion>

<Accordion title="Integration fields">
  <ResponseField name="campaignId" type="string">Campaign reference</ResponseField>
  <ResponseField name="hsContactId" type="string">HubSpot contact ID</ResponseField>
  <ResponseField name="hsPortalId" type="string">HubSpot portal ID</ResponseField>
  <ResponseField name="ghlContactId" type="string">GHL contact ID</ResponseField>
  <ResponseField name="ghlLocationId" type="string">GHL location ID</ResponseField>
  <ResponseField name="ghlConversationMessageId" type="string">GHL conversation message ID</ResponseField>
</Accordion>

<ResponseField name="createdAt" type="string">Creation timestamp</ResponseField>
<ResponseField name="updatedAt" type="string">Last update timestamp</ResponseField>

***

## Status lifecycle

```
queued → pending → sending → sent → delivered
                               ↘ failed
                               ↘ fallback
                          ↗ scheduled (rescheduled)
                     cancelled
```

| 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 (iMessage relay / 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                                                                   |

<Tip>
  **`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.
</Tip>

***

## 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**                       |

<Note>
  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`.
</Note>

***

## Retries

| Context                       | Send attempts                            | Verification between attempts                                         | Gap | Env vars                |
| ----------------------------- | ---------------------------------------- | --------------------------------------------------------------------- | --- | ----------------------- |
| **Individual** (API / Unibox) | **5**                                    | `outgoing_messages` + device gateway `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 | —                                                                     | —   | —                       |

<Info>
  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.
</Info>

***

## 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

| 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.                                 |
