---
title: Sending Messages | API Docs
description: How to send text, media, and rich messages with the Linq API.
---

The Linq API lets you send messages containing text, media, or a mix of both across iMessage, RCS, and SMS.

## Starting a conversation

There are two ways to open a conversation with a new recipient, depending on whether you want Linq to choose the sending line for you.

### Let Linq pick the line

`POST /v3/messages` takes the recipients and the message and nothing else — no `from`. Linq resolves both the line and the chat: it reuses the chat those recipients are already in while that line can still send, opens a new chat on the best available line when there isn’t one, and moves the recipients onto a fresh line when their current one can’t send. Traffic spreads across your pool on its own, and lines you add later enter the rotation with no code change.

- [cURL](#tab-panel-0-0)
- [TypeScript](#tab-panel-0-1)
- [Python](#tab-panel-0-2)
- [Go](#tab-panel-0-3)

Terminal window

```
curl -X POST https://api.linqapp.com/api/partner/v3/messages \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "to": [
        "+14155559876"
      ],
      "message": {
        "parts": [
          {
            "type": "text",
            "value": "Hi! Thanks for reaching out — how can we help?"
          }
        ]
      }
    }'
```

```
await client.messages.create({
  to: ["+14155559876"],
  message: {
    parts: [
      {
        type: "text",
        value: "Hi! Thanks for reaching out — how can we help?",
      },
    ],
  },
});
```

```
client.messages.create(
    to=["+14155559876"],
    message={
        "parts": [
            {
                "type": "text",
                "value": "Hi! Thanks for reaching out — how can we help?",
            },
        ],
    },
)
```

```
client.Messages.Create(context.TODO(), linq.MessageNewParams{
  To: linq.F([]string{"+14155559876"}),
  Message: linq.F(map[string]any{
    Parts: linq.F([]any{
      map[string]any{
        Type: linq.F("text"),
        Value: linq.F("Hi! Thanks for reaching out — how can we help?"),
      },
    }),
  }),
})
```

The response says exactly what happened:

| Field                         | What it tells you                                                   |
| ----------------------------- | ------------------------------------------------------------------- |
| `from`                        | The line the message actually went out on                           |
| `chat_id`, `created_new_chat` | Which chat it landed in, and whether that chat is new               |
| `from_selection.reason`       | `reused_active_chat`, `new_best_number`, or `failover_flagged`      |
| `previous_chat_id`            | Set on `failover_flagged` only — the abandoned chat on the old line |

Two optional fields shape the pick:

- **`continuation_message`** — text-only, sent *instead of* `message` when the send fails over to a new line. Useful as an opener that reads sensibly coming from an unfamiliar number. Ignored on reuse and on genuine first contact; exactly one message is sent either way.
- **`exclude_from`** — lines to keep out of *this* send’s pick. It only affects picking a line for a **new** chat; an existing chat is always reused on its own line, so an exclusion never abandons a live conversation. Nothing is remembered between calls.

Unlike `POST /v3/chats`, this endpoint allows a link in the first message — though a link as the very first message on a freshly selected line can elevate that line’s flagging risk. Voice memos aren’t supported here; use the [voice memo endpoint](/channel/imessage/guides/messaging/voice-memos/index.md) with a known chat ID.

See the [Send Message API reference](/channel/imessage/api/resources/messages/methods/create/index.md) for the full request and response schema, and [Just hit send: introducing managed load balancing](https://linqapp.com/blog/just-hit-send-introducing-managed-load-balancing) for the background on why this is the default path.

### Create the chat on a specific line

When a conversation has to go out on a particular line — a dedicated number per brand, say — create the chat yourself with an explicit `from`. The API creates the conversation and sends the message in a single request:

- [cURL](#tab-panel-1-0)
- [TypeScript](#tab-panel-1-1)
- [Python](#tab-panel-1-2)
- [Go](#tab-panel-1-3)

Terminal window

```
curl -X POST https://api.linqapp.com/api/partner/v3/chats \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "from": "+12052535597",
      "to": [
        "+12052532136"
      ],
      "message": {
        "parts": [
          {
            "type": "text",
            "value": "Hello! How can I help you today?"
          }
        ]
      }
    }'
```

```
await client.chats.create({
  from: "+12052535597",
  to: ["+12052532136"],
  message: {
    parts: [
      {
        type: "text",
        value: "Hello! How can I help you today?",
      },
    ],
  },
});
```

```
client.chats.create(
    from_="+12052535597",
    to=["+12052532136"],
    message={
        "parts": [
            {
                "type": "text",
                "value": "Hello! How can I help you today?",
            },
        ],
    },
)
```

```
client.Chats.Create(context.TODO(), linq.ChatNewParams{
  From: linq.F("+12052535597"),
  To: linq.F([]string{"+12052532136"}),
  Message: linq.F(map[string]any{
    Parts: linq.F([]any{
      map[string]any{
        Type: linq.F("text"),
        Value: linq.F("Hello! How can I help you today?"),
      },
    }),
  }),
})
```

A few constraints on this request:

- `from` is required and must be a phone number assigned to your account. Choosing the line yourself opts the conversation out of the balancing and failover above, so check the line’s [reputation](/channel/imessage/guides/phone-numbers/phone-reputation/index.md) before you send and handle failover yourself.
- `to` accepts an array — one recipient for a direct message, multiple for a [group chat](/channel/imessage/guides/chats/group-chats/index.md).
- The first outbound message must not contain links. `link` parts and text parts containing URLs are rejected on `POST /v3/chats` — send the initial message without links, then follow up with a [link preview](/channel/imessage/guides/messaging/rich-link-previews/index.md) using the returned chat ID.

See the [Create Chat](/channel/imessage/api/resources/chats/methods/create/index.md) endpoint for the full request schema.

## Sending to an existing chat

Once you have a chat ID, post follow-up messages directly to it. The request body is a `message` object with a `parts` array — see the [Send Message API reference](/channel/imessage/api/resources/chats/subresources/messages/methods/send/index.md) for the full schema and language-specific examples.

## Message parts

Messages use a `parts` array where each part is `text`, `media`, `link`, or `app_clip`. You can mix text and media in a single message; `link` and `app_clip` parts must each be sent on their own.

**Text part:**

```
{ "type": "text", "value": "Hello!" }
```

**Media part (direct URL):**

```
{
  "type": "media",
  "url": "https://example.com/photo.jpg"
}
```

**Media part (pre-uploaded attachment):**

```
{
  "type": "media",
  "attachment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

**Link part (rich link preview):**

```
{
  "type": "link",
  "value": "https://linqapp.com"
}
```

**App Clip part:**

```
{
  "type": "app_clip",
  "value": "https://zero.linqapp.com/pay/acme?session=tok_...",
  "caption": "Play now"
}
```

Sends a registered App Clip — Linq’s Apple Pay checkout, or a partner’s own App Clip. iMessage only, and it must be the only part in the message. `caption` is optional — the label on the App Clip’s **Open** button; omit it to use the App Clip’s own default (`Tap open`). The payment-checkout use is covered in [Sending payment links → Send it as an App Clip card](/channel/imessage/guides/payments/sending-payment-links#send-it-as-an-app-clip-card/index.md).

**Mixed message (text + image) — full request body:**

```
{
  "message": {
    "parts": [
      { "type": "text", "value": "Check out this photo!" },
      { "type": "media", "url": "https://example.com/photo.jpg" }
    ]
  }
}
```

> **Note:** Media parts reference the file by `url` or `attachment_id`. MIME type is inferred server-side — you do not need to pass a `mime_type` field.

**Limits:**

- Up to **100 parts** per message
- Up to **40 public-URL media parts** per message (pre-uploaded attachments are exempt)
- Text `value` max length: **10,000** characters
- Link `value` (URL) max length: **2,048** characters
- Link parts must be the **only** part in a message — they render as a rich preview on iMessage and RCS
- `app_clip` parts must be the **only** part in a message — iMessage only, and they never downgrade to SMS or RCS (see [Sending payment links](/channel/imessage/guides/payments/sending-payment-links#send-it-as-an-app-clip-card/index.md))
- Consecutive text parts are **not allowed** — separate them with media or send as individual messages

See [Attachments](/channel/imessage/guides/messaging/attachments/index.md) for details on sending media files.

## Text decorations

Apply inline styles and animations to character ranges within a text part using the `text_decorations` array. Each decoration specifies a `range: [start, end)` (inclusive–exclusive, measured in UTF-16 code units) and exactly one of `style` or `animation`. Decorations are iMessage-only and ignored on RCS and SMS (see [Protocol Selection](/channel/imessage/guides/messaging/protocol-selection/index.md)).

```
{
  "type": "text",
  "value": "Hello world",
  "text_decorations": [
    { "range": [0, 5], "style": "bold" },
    { "range": [6, 11], "animation": "shake" }
  ]
}
```

**Styles:** `bold` , `italic` , `strikethrough` , `underline`

**Animations:** `big` , `small` , `shake` , `nod` , `explode` , `ripple` , `bloom` , `jitter`

Style ranges may overlap (e.g. bold + italic on the same characters), but animation ranges must not overlap with other animations or styles.

## Mentions

Address one participant of a group chat by setting `mention` (and optionally `mention_range`) on a text part. Group chats only, and iMessage recipients are notified through a mute. See [Mentions](/channel/imessage/guides/messaging/mentions/index.md).

## Replying to messages

Thread messages by referencing a specific message ID:

- [cURL](#tab-panel-2-0)
- [TypeScript](#tab-panel-2-1)
- [Python](#tab-panel-2-2)
- [Go](#tab-panel-2-3)

Terminal window

```
curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/messages \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "message": {
        "parts": [
          {
            "type": "text",
            "value": "Great point!"
          }
        ],
        "reply_to": {
          "message_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
          "part_index": 0
        }
      }
    }'
```

```
await client.chats.messages.send({chatId}, {
  message: {
    parts: [
      {
        type: "text",
        value: "Great point!",
      },
    ],
    reply_to: {
      message_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
      part_index: 0,
    },
  },
});
```

```
client.chats.messages.send(
    {chat_id},
    message={
        "parts": [
            {
                "type": "text",
                "value": "Great point!",
            },
        ],
        "reply_to": {
            "message_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
            "part_index": 0,
        },
    },
)
```

```
client.Chats.Messages.Send(context.TODO(), {chatId}, linq.ChatMessageSendParams{
  Message: linq.F(map[string]any{
    Parts: linq.F([]any{
      map[string]any{
        Type: linq.F("text"),
        Value: linq.F("Great point!"),
      },
    }),
    ReplyTo: linq.F(map[string]any{
      MessageId: linq.F("6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
      PartIndex: linq.F(0),
    }),
  }),
})
```

The `part_index` field is optional — use it to reply to a specific part of a multipart message (0-indexed). To walk a thread after building it, see [Message Details → Get thread messages](/channel/imessage/guides/messaging/message-details#get-thread-messages/index.md).

## Message effects

Add iMessage screen or bubble effects (confetti, fireworks, slam, invisible ink, etc.) by including an `effect` object inside `message`. iMessage only — silently ignored on RCS and SMS. See [Message Effects](/channel/imessage/guides/messaging/message-effects/index.md) for the full list and an example.

## Protocol selection

Force a specific delivery protocol with `preferred_service` (`iMessage`, `RCS`, or `SMS`) inside `message`. If omitted, the API picks the best available. See [Protocol Selection](/channel/imessage/guides/messaging/protocol-selection/index.md) for behavior and tradeoffs.

## Idempotency

Include an `idempotency_key` field inside the `message` object to prevent duplicate sends on retries. It goes inside the `message` body, not as an HTTP header. Maximum length is 255 characters.

- [cURL](#tab-panel-3-0)
- [TypeScript](#tab-panel-3-1)
- [Python](#tab-panel-3-2)
- [Go](#tab-panel-3-3)

Terminal window

```
curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/messages \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "message": {
        "parts": [
          {
            "type": "text",
            "value": "This won't be sent twice"
          }
        ],
        "idempotency_key": "unique-request-id-123"
      }
    }'
```

```
await client.chats.messages.send({chatId}, {
  message: {
    parts: [
      {
        type: "text",
        value: "This won't be sent twice",
      },
    ],
    idempotency_key: "unique-request-id-123",
  },
});
```

```
client.chats.messages.send(
    {chat_id},
    message={
        "parts": [
            {
                "type": "text",
                "value": "This won't be sent twice",
            },
        ],
        "idempotency_key": "unique-request-id-123",
    },
)
```

```
client.Chats.Messages.Send(context.TODO(), {chatId}, linq.ChatMessageSendParams{
  Message: linq.F(map[string]any{
    Parts: linq.F([]any{
      map[string]any{
        Type: linq.F("text"),
        Value: linq.F("This won't be sent twice"),
      },
    }),
    IdempotencyKey: linq.F("unique-request-id-123"),
  }),
})
```

The same applies when creating a chat (`POST /v3/chats`) — the key goes inside the nested `message` object:

```
{
  "from": "+12223334444",
  "to": ["+15556667777"],
  "message": {
    "parts": [{ "type": "text", "value": "Hello" }],
    "idempotency_key": "unique-request-id-123"
  }
}
```

If a message with the same key has already been processed, the API returns the original response instead of sending again.

> **Tip:** Use UUIDs or other globally unique values as idempotency keys, and always set one in production to handle network retries safely. The official [SDKs](/channel/imessage/getting-started/sdks/index.md) accept the key via a method option.

## Editing messages

Edit a single text part of a previously sent message by passing `part_index` (0-based) and the new `text`. Only text parts are editable. iMessage only — listen for the [`message.edited`](/channel/imessage/guides/webhooks/events#message-events/index.md) webhook to confirm the edit was applied. Editable up to **5 times** within **15 minutes** of the original send. See the [Edit Message API reference](/channel/imessage/api/resources/messages/methods/update/index.md).

## Deleting messages

Delete removes a message from Linq’s records but does **not** unsend it — recipients still see the message on their device. See the [Delete Message API reference](/channel/imessage/api/resources/messages/methods/delete/index.md).
