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

# Send Message

> Send an iMessage, SMS, or email to a contact — the core automation endpoint. REST endpoint in the Tuco AI iMessage API — bearer-token auth, JSON.

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

## Authentication

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

```bash theme={null}
Authorization: Bearer tuco_sk_xxxxxxxxxxxxx
```

***

## Request body

<ParamField body="message" type="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.
</ParamField>

<ParamField body="attachmentUrls" type="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.

  <Tip>
    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.
  </Tip>
</ParamField>

<ParamField body="recipientPhone" type="string">
  Phone number in E.164 format (e.g. `"+12025551234"`).
  Required unless `recipientEmail` or `leadId` is provided.
</ParamField>

<ParamField body="recipientEmail" type="string">
  Email address for email or iMessage (Apple ID).
  Required unless `recipientPhone` or `leadId` is provided.
</ParamField>

<ParamField body="messageType" type="string" default="imessage">
  Channel to use. When omitted, defaults to `"imessage"`.

  | Value        | Channel                   | Addresses tried (from lead)               |
  | ------------ | ------------------------- | ----------------------------------------- |
  | `"imessage"` | iMessage via device relay | phone → altPhone1-3 → email → altEmail1-3 |
  | `"sms"`      | SMS via Twilio            | phone → altPhone1-3 only                  |
  | `"email"`    | Email                     | email → altEmail1-3 only                  |
</ParamField>

<ParamField body="fromLineId" type="string">
  Tuco line ID to send from. **Optional** — when omitted, Tuco round-robins
  across your workspace's active lines automatically.
</ParamField>

<ParamField body="leadId" type="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.
</ParamField>

<ParamField body="recipientName" type="string">
  Display name for the recipient. Derived from the lead when omitted.
</ParamField>

<Accordion title="Alternate contact fields">
  Stored on the lead (created or found). The worker tries these in priority
  order when the primary address fails iMessage availability.

  <ParamField body="altPhone1" type="string">Alternate phone number 1</ParamField>
  <ParamField body="altPhone2" type="string">Alternate phone number 2</ParamField>
  <ParamField body="altPhone3" type="string">Alternate phone number 3</ParamField>
  <ParamField body="altEmail1" type="string">Alternate email 1</ParamField>
  <ParamField body="altEmail2" type="string">Alternate email 2</ParamField>
  <ParamField body="altEmail3" type="string">Alternate email 3</ParamField>
</Accordion>

<Accordion title="Scheduling & time window">
  Control **when** the message is sent. All fields are optional.
  When omitted, the message sends immediately (subject to line limits and device gaps).

  <ParamField body="scheduledDate" type="string">
    ISO 8601 timestamp to send in the future (e.g. `"2025-10-15T14:30:00Z"`).
  </ParamField>

  <ParamField body="timezone" type="string">
    IANA timezone for the send window (e.g. `"America/New_York"`). Required
    if using `sendWindowStart`/`sendWindowEnd`.
  </ParamField>

  <ParamField body="sendWindowStart" type="string">
    Earliest time to send, `HH:mm` format (e.g. `"09:00"`).
  </ParamField>

  <ParamField body="sendWindowEnd" type="string">
    Latest time to send, `HH:mm` format (e.g. `"17:00"`).
  </ParamField>

  <ParamField body="allowedDaysOfWeek" type="number[]">
    Days when sending is allowed. `0` = Sunday, `6` = Saturday.
    Example: `[1,2,3,4,5]` for weekdays only.
  </ParamField>
</Accordion>

<Accordion title="Fallback & advanced">
  <ParamField body="sendFallbackSmsOnFailed" type="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.
  </ParamField>

  <ParamField body="forceFallback" type="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.
  </ParamField>

  <ParamField body="skipIfReplied" type="string | boolean" default="false">
    Workspace-level reply gate. When set, Tuco **suppresses this send if the
    contact has already replied in your workspace** — the same protection
    campaigns get from "stop on reply", now available per request.

    * `false` / `"false"` / omitted → always send (default, backward compatible)
    * `true` / `"true"` → skip if the contact has **ever** replied
    * a window string → skip only if they replied **within** that window:
      `"1m"`, `"1h"`, `"1d"`, `"2d"`, `"5d"`, `"7d"`, `"14d"`, `"1mo"`, `"1yr"`
      (`m`=minute, `h`=hour, `d`=day, `w`=week, `mo`=30 days, `yr`=365 days)

    Scope is your **workspace** — a reply to *any* of your lines counts (not just
    the sending line). The check never crosses workspaces. On a skip, **no message
    is created** and the API returns `200` with `skippedOnly: true` (see
    [Skip if the contact already replied](#skip-if-the-contact-already-replied)).
    An unrecognized value returns `400 INVALID_SKIP_IF_REPLIED`.
  </ParamField>

  <ParamField body="batchId" type="string">
    Free-form string to group related messages for reporting.
  </ParamField>

  <ParamField body="correlationId" type="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](/api-reference/introduction#correlation-ids-request-tracing).
  </ParamField>
</Accordion>

<Accordion title="Request headers">
  Headers Tuco recognizes in addition to `Authorization`.

  | Header              | Purpose                                                                                                                             |
  | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
  | `x-correlation-id`  | Request-scoped tracing ID (preferred over body field). Echoed back in Loki under `correlationId`.                                   |
  | `x-execution-id`    | GHL workflow execution ID. Only set when calling from a GHL workflow context.                                                       |
  | `x-ghl-workflow-id` | GHL workflow definition ID. Pairs with `x-execution-id`.                                                                            |
  | `Idempotency-Key`   | When supplied, Tuco caches the response and replays it on retry within the idempotency window. Use one unique key per logical send. |
</Accordion>

***

## Examples

Create or import a lead first using [`POST /api/leads`](https://docs.tuco.ai/api-reference/leads), then send using `leadId` as shown below.

<CodeGroup>
  ```bash Minimal using leadId (defaults to iMessage) theme={null}
  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"
    }'
  ```

  Need a `leadId` first? Create one with [`POST /api/leads`](https://docs.tuco.ai/api-reference/leads), then use the returned `leadId` in this request.

  ```bash HubSpot lead theme={null}
  curl -X POST "https://app.tuco.ai/api/messages" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "message": "Hi from Tuco.",
      "leadId": "667f1f77bcf86cd799439013",
      "hsContactId": "120001",
      "hsPortalId": "991001"
    }'
  ```

  ```bash GHL lead theme={null}
  curl -X POST "https://app.tuco.ai/api/messages" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "message": "Hi from Tuco.",
      "leadId": "667f1f77bcf86cd799439014",
      "ghlContactId": "ghl_contact_001",
      "ghlLocationId": "eL7DD22BdZ0rismu7qCA"
    }'
  ```

  ```bash With attachments theme={null}
  curl -X POST "https://app.tuco.ai/api/messages" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "message": "Here is the file.",
      "leadId": "667f1f77bcf86cd799439012",
      "attachmentUrls": ["https://example.com/doc.pdf"]
    }'
  ```

  ```bash GoHighLevel webhook (raw payload) theme={null}
  # The exact body GoHighLevel's "Webhook" workflow action posts. You only fill in
  # customData.message — Tuco derives the recipient + GHL identity from the rest.
  curl -X POST "https://app.tuco.ai/api/messages" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "contact_id": "KWqc16yBbIV7hLfiRTse",
      "full_name": "Matheus Maia",
      "email": "matheus@gmail.com",
      "phone": "+13057336577",
      "location": { "id": "57m52yUR4RkI2k1VuHhL" },
      "customData": { "message": "Hi Matheus" }
    }'
  ```
</CodeGroup>

***

## Send straight from a GoHighLevel webhook

You can point a GoHighLevel **Workflow → Webhook** action directly at this endpoint —
no GHL Marketplace app or action secret required, just your Tuco API key in the
`Authorization` header. GHL's Webhook action already includes the full contact
payload; Tuco reads GHL's native field names so **the only thing you map in Custom
Data is `message`**.

<Steps>
  <Step title="Add a Webhook action to your workflow">
    Method `POST`, URL `https://app.tuco.ai/api/messages`.
  </Step>

  <Step title="Add the auth header">
    `Authorization` = `Bearer tuco_sk_xxxxxxxxxxxxx`.
  </Step>

  <Step title="Add Custom Data">
    Set `message` (required). Optionally add `lineId` (pin a sending line — otherwise
    Tuco round-robins) and `attachmentUrls` (a single URL or comma-separated list).
  </Step>
</Steps>

Tuco auto-maps GHL's native fields onto the canonical request fields. **Custom Data
always wins** — set any field there to override what's derived from the contact:

| Canonical field     | Filled from (Custom Data first, then GHL native)                      |
| ------------------- | --------------------------------------------------------------------- |
| `message`           | `customData.message`                                                  |
| `fromLineId`        | `customData.lineId`                                                   |
| `attachmentUrls`    | `customData.attachmentUrls` (string or CSV → array)                   |
| `recipientPhone`    | `customData.recipientPhone` → top-level `phone`                       |
| `recipientEmail`    | `customData.recipientEmail` → top-level `email`                       |
| `recipientName`     | `customData.recipientName` → `full_name` → `first_name` + `last_name` |
| `ghlContactId`      | `customData.ghlContactId` → `contact_id`                              |
| `ghlLocationId`     | `customData.ghlLocationId` → `location.id`                            |
| `contactOwnerEmail` | `customData.contactOwnerEmail` → `user.email`                         |

<Note>
  This mapping is **additive and backward compatible** — it only fills a field you did
  not already send. Existing integrations that post canonical fields (`recipientPhone`,
  `ghlContactId`, …) are completely unaffected.
</Note>

<Warning>
  If `{{contact.phone}}` is empty on the contact, the request fails with
  `missing_recipient`. Make sure the contact has a phone or email before the webhook fires.
</Warning>

***

## Response

The API always returns **`201`** when the message is successfully created.
The `status` field tells you what happens next.

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

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

```json theme={null}
{
  "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.

<ResponseField name="success" type="boolean">
  `true` when the message was created.
</ResponseField>

<ResponseField name="status" type="string">
  Current status of the message. See the status table below.
</ResponseField>

<ResponseField name="message" type="object">
  The full message document. Key fields:

  <Expandable title="Message fields">
    <ResponseField name="_id" type="string">Unique message ID</ResponseField>
    <ResponseField name="message" type="string">Body text</ResponseField>
    <ResponseField name="messageType" type="string">Channel (`imessage`, `sms`, `email`)</ResponseField>
    <ResponseField name="status" type="string">Lifecycle status</ResponseField>
    <ResponseField name="fromLineId" type="string">Line used to send</ResponseField>
    <ResponseField name="recipientPhone" type="string">Recipient phone</ResponseField>
    <ResponseField name="recipientEmail" type="string">Recipient email</ResponseField>
    <ResponseField name="leadId" type="string">Linked lead ID</ResponseField>
    <ResponseField name="scheduledDate" type="string">When the message will send (if scheduled)</ResponseField>
    <ResponseField name="sentAt" type="string">When actually sent</ResponseField>
    <ResponseField name="deliveredAt" type="string">When delivery confirmed</ResponseField>
    <ResponseField name="createdAt" type="string">Creation timestamp</ResponseField>
  </Expandable>
</ResponseField>

### Status values

| Status        | Meaning                                                                            | Is 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 gap                     | **No**                                 |
| `"fallback"`  | Recipient has no iMessage; fallback SMS sent if configured                         | **No** (business rule)                 |
| `"failed"`    | All retries exhausted or availability API error                                    | Yes (technical)                        |

<Info>
  `"pending"` and `"scheduled"` mean the message is accepted and queued. The worker sends it when:

  * **Time window** is satisfied (inside `sendWindowStart`–`sendWindowEnd`)
  * **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.
</Info>

***

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

```json HTTP 200 — duplicate caught theme={null}
{
  "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.

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

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

For deeper detail, edge cases, and ops events, see
[Atomic Duplicate Guard](/imessage-double-send-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.

***

## Skip if the contact already replied

Set `skipIfReplied` to avoid following up with a contact who has already responded
to you. Tuco checks your workspace's inbound history for this recipient (phone or
email) and, if it finds a reply, **does not create or send the message**. This is the
per-request version of the campaign "stop on reply" rule — useful for drip
automations in n8n/Make/GHL where you don't want to nudge someone who already wrote
back.

### Values

| `skipIfReplied`                                                   | Behavior                                                    |
| ----------------------------------------------------------------- | ----------------------------------------------------------- |
| omitted / `false` / `"false"`                                     | Always send (default).                                      |
| `true` / `"true"`                                                 | Skip if the contact has **ever** replied in your workspace. |
| `"1m"` `"1h"` `"1d"` `"2d"` `"5d"` `"7d"` `"14d"` `"1mo"` `"1yr"` | Skip only if they replied **within** that window.           |

Units: `m` = minute, `h` = hour, `d` = day, `w` = week, `mo` = 30 days, `yr` = 365 days.
General form: `<number><unit>` (e.g. `"3d"`, `"36h"`). Note `"1m"` is one **minute** and
`"1mo"` is one **month**.

<Note>
  **Scope is your workspace, not a single line.** A reply to *any* of your lines counts —
  which line you send from doesn't matter. The check is strictly scoped to your workspace
  and never sees another workspace's replies.
</Note>

### Request

```bash Skip the follow-up if they replied in the last 7 days theme={null}
curl -X POST "https://app.tuco.ai/api/messages" \
  -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Just following up — still interested?",
    "recipientPhone": "+12025551234",
    "skipIfReplied": "7d"
  }'
```

### Response when the send is skipped (`200 OK`)

No message is created. `success` is `true` (the request was handled — do **not** retry)
and `skippedOnly` distinguishes it from a real send.

```json HTTP 200 — skipped because the contact already replied theme={null}
{
  "success": true,
  "skippedOnly": true,
  "skipReason": "recipient_replied",
  "skipMessage": "Skipped: this contact already replied in your workspace within the last 7d. The skipIfReplied gate prevented this send.",
  "repliedAt": "2026-06-27T18:40:11.020Z",
  "message": null,
  "leadId": "667f1f77bcf86cd799439012"
}
```

Branch on `skippedOnly === true` and treat it as a successful no-op. When the contact
has **not** replied (or replied outside the window), the request proceeds exactly as a
normal send and returns the usual `201` response.

<Warning>
  An unrecognized `skipIfReplied` value returns **400** with
  `code: "INVALID_SKIP_IF_REPLIED"`. Use `true`, `false`, or one of the documented
  window strings.
</Warning>

Every decision is logged to Loki (`message.api.skipped_replied`,
`message.api.skip_if_replied_passed`, `message.api.skip_if_replied_invalid`) under your
`correlationId`, so you can audit exactly why a send was or wasn't suppressed.

***

## Errors

Errors are returned only for **validation failures** — never for pre-send checks.

| Status | When                                                        | Example                                                                 |
| ------ | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| `400`  | Message and attachments both empty                          | `{ "error": "Message and attachmentUrls cannot both be empty" }`        |
| `400`  | Invalid `messageType`                                       | `{ "error": "Invalid messageType. Must be email, sms, or imessage" }`   |
| `400`  | No active lines + no `fromLineId`                           | `{ "error": "No active lines in workspace..." }`                        |
| `400`  | No recipient anywhere                                       | `{ "error": "Either recipientEmail or recipientPhone is required..." }` |
| `400`  | `sendFallbackSmsOnFailed: true` but fallback not configured | `{ "error": "...", "code": "FALLBACK_NOT_CONFIGURED" }`                 |
| `400`  | Unrecognized `skipIfReplied` value                          | `{ "error": "...", "code": "INVALID_SKIP_IF_REPLIED" }`                 |
| `401`  | Invalid or missing API key                                  | `{ "error": "Unauthorized" }`                                           |
| `402`  | Subscription past due (workspace read-only)                 | `{ "error": "READ_ONLY", "code": "READ_ONLY", "reason": "past_due" }`   |
| `404`  | `leadId` provided but not found                             | `{ "error": "Lead not found or access denied" }`                        |

***

## What happens after the API call

<Steps>
  <Step title="Message created">
    Tuco inserts a record in the `messages` collection with status `pending`
    (or `scheduled` if `scheduledDate` is provided).
  </Step>

  <Step title="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.
  </Step>

  <Step title="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).

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

  <Step title="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.
  </Step>

  <Step title="Outcome">
    `sent` → appears in Unibox + webhook fires.
    `failed` → error recorded + optional fallback SMS.
    `delivered` → confirmed later via device callbacks.
  </Step>
</Steps>

***

## Lead resolution

When you send without a `leadId`, Tuco resolves the recipient automatically:

<AccordionGroup>
  <Accordion title="Lead found by phone/email">
    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).
  </Accordion>

  <Accordion title="New lead created (Quick Sends)">
    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.
  </Accordion>

  <Accordion title="leadId provided">
    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.
  </Accordion>
</AccordionGroup>

***

## Alt contact fields

When you pass `altPhone1`–`altPhone3` or `altEmail1`–`altEmail3`, 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
```

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

***

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

<Warning>
  If you set `sendFallbackSmsOnFailed: true` but fallback is **not** configured,
  the API returns **400** with `code: "FALLBACK_NOT_CONFIGURED"`.
</Warning>

| Scenario                                                              | Fallback SMS fires?            |
| --------------------------------------------------------------------- | ------------------------------ |
| Status = `fallback` (no iMessage), Twilio configured                  | **Yes** — always               |
| Status = `failed`, `sendFallbackSmsOnFailed: true`, Twilio configured | **Yes**                        |
| Status = `failed`, `sendFallbackSmsOnFailed: false` (default)         | **No**                         |
| Status = `failed`, flag is `true` but no Twilio                       | **400 error at API call time** |

<Note>
  Fallback SMS requires `recipientPhone` on the message. Email-only messages
  will not trigger fallback SMS.
</Note>
