Skip to content
Linq

Create a new chat

POST/v3/chats

Create a new chat with specified participants and send an initial message. The initial message is required when creating a chat.

To protect sender deliverability, the first outbound message of a new chat cannot be a link. The request is rejected with 400 (error code 1005) when:

  • The message contains a link part (explicit rich-preview link), or
  • Any text part contains a URL.

This rule applies only to POST /v3/chats. Follow-up messages on an existing chat (POST /v3/chats/{chatId}/messages) are not subject to this restriction.

Reusing an Existing Chat

Chats are keyed on the from line plus the exact set of to handles. Repeating this request with the same from and to returns the existing chat and sends the message into it instead of starting a second conversation.

A new chat is created instead of reusing one when the participant set changed (a participant was added or removed), or the from line left the group.

Whenever the response is a new chat, the first-message rules above apply to that request: no link in the first message, and no reply_to. To send into a chat you already know, use POST /v3/chats/{chatId}/messages with its chat_id.

Body ParametersJSONExpand Collapse
from: string

Sender phone number in E.164 format. Must be a phone number that the authenticated partner has permission to send from.

message: MessageContent { parts, idempotency_key, reply_to }

Message content container. Groups all message-related fields together, separating the “what” (message content) from the “where” (routing fields like from/to).

parts: array of TextPart { type, value } or MediaPart { type, attachment_id, url } or LinkPart { type, value }

The message’s one part: text, media, or link. An RCS message carries exactly one part; send text and media as separate messages.

Rich Link Previews:

  • Use a link part to send a URL with a rich preview card
  • A URL inside a text part renders a preview too

Supported Media:

  • Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp
  • Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp
  • Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr
  • Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm
  • Contact & Calendar: .vcf, .ics

Audio:

  • Audio files (.m4a, .mp3, .aac, .caf, .wav, .aiff, .amr) are fully supported as media parts

Validation Rules:

  • Exactly one part per message.
One of the following:
TextPart object { type, value }
type: "text"

Indicates this is a text message part

value: string

The text content of the message. This value is sent as-is with no parsing or transformation — Markdown syntax will be delivered as plain text.

minLength1
maxLength10000
MediaPart object { type, attachment_id, url }
type: "media"

Indicates this is a media attachment part

attachment_id: optional string

Reference to a file pre-uploaded via POST /v3/attachments (optional). The file is already stored, so sends using this ID skip the download step — useful when sending the same file to many recipients.

Either url or attachment_id must be provided, but not both.

formatuuid
url: optional string

Any publicly accessible HTTPS URL to the media file. The server downloads and sends the file automatically — no pre-upload step required.

Size limit: 10MB maximum for URL-based downloads. For larger files (up to 100MB), use the pre-upload flow: POST /v3/attachments to get a presigned URL, upload directly, then reference by attachment_id.

Requirements:

  • URL must use HTTPS
  • File content must be a supported format (the server validates the actual file content)

Supported formats:

  • Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp
  • Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp
  • Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr
  • Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm
  • Contact & Calendar: .vcf, .ics

Tip: Audio sent here appears as a regular file attachment. For repeated sends of the same file, use attachment_id to avoid redundant downloads.

Either url or attachment_id must be provided, but not both.

formaturi
idempotency_key: optional string

Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. Reusing a key whose message was deleted returns 404; the message is never resent.

maxLength255
reply_to: optional ReplyTo { message_id, part_index }

Reply to another message to create a threaded conversation

message_id: string

The ID of the message to reply to

formatuuid
part_index: optional number

The specific message part to reply to (0-based index). Defaults to 0 (first part) if not provided. Use this when replying to a specific part of a multipart message.

formatint32
minimum0
to: array of string

Array of recipient handles (phone numbers in E.164 format or email addresses). For individual chats, provide one recipient. For group chats, provide multiple.

override_optout: optional boolean

Send even though the recipient asked you to stop (403, error code 2024). Applies to this request only: the opt-out stays in place, so the next send without this flag is rejected again. Every override is recorded against your API key.

ReturnsExpand Collapse
chat: object { id, display_name, handles, 4 more }
id: string

Unique identifier for the created chat (UUID)

formatuuid
display_name: string

Display name for the chat. Defaults to a comma-separated list of recipient handles. Can be updated for group chats.

handles: array of ChatHandle { id, handle, joined_at, 4 more }

List of participants in the chat. Always contains at least two handles (your phone number and the other participant).

id: string

Unique identifier for this handle

formatuuid
handle: string

Phone number (E.164) or email address of the participant

joined_at: string

When this participant joined the chat

formatdate-time
service: ServiceType

Messaging service type. Where this names the transport a message used, it is per-message: a chat’s own service can differ from a message in it, and a conversation can fall back from RCS to SMS.

One of the following:
"SMS"
"RCS"
is_me: optional boolean

Whether this handle belongs to the sender (your phone number)

left_at: optional string

When they left (if applicable)

formatdate-time
status: optional "active" or "left" or "removed"

Participant status

One of the following:
"active"
"left"
"removed"
health_status: object { doc_url, status, updated_at }

[BETA] Current health for a chat. Always present — chats start at HEALTHY and may shift based on engagement and delivery signals on the conversation. Many AT_RISK or CRITICAL chats on a single line increase the risk of line flagging.

Switch on status to surface chat and line health in your UI — the enum is the long-term contract. Each status carries a doc_url that deep-links to the relevant section of the Chat Health guide. To gate a send, act on the response rather than the status: a 403 is the authoritative answer.

See the Chat Health guide for what each status means and how to react.

doc_url: string

Deep-link to the relevant section of the Chat Health guide for this status.

formaturi
status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"

Current health bucket for the chat. See the Chat Health guide for what each value means and how to react. doc_url deep-links to the relevant section.

OPTED_OUT — the recipient sent STOP, UNSUBSCRIBE, OPTOUT, CANCEL, END, or QUIT. The keyword must be the whole trimmed message, never part of a longer one: STOP counts, please stop does not. Most keywords must match exactly, including case. OPT OUT is the exception — it matches in any casing, with or without the space or a hyphen, so opt out, Opt-Out and optout all count. It clears as soon as they reply again: any later message from them that is not itself an opt-out keyword opts them back in immediately — a reply in any conversation with you counts, the same way the block does.

OPTED_OUT marks only the conversation the keyword arrived in. The block below is wider than the mark, so a conversation still reading HEALTHY can be blocked as well — gate on the 403, not on the status. Group threads are never marked and are never blocked.

Linq enforces this: while a recipient is opted out, every send to them is rejected with 403 (error code 2024) before the message is queued, across every chat and every line on your account. Nothing is delivered, including a final courtesy message — to send one, set override_optout: true on that single request.

One of the following:
"HEALTHY"
"AT_RISK"
"CRITICAL"
"OPTED_OUT"
updated_at: string

When this status last changed.

formatdate-time
is_group: boolean

Whether this is a group chat

message: SentMessage { id, created_at, delivery_status, 7 more }

A message that was sent (used in CreateChat and SendMessage responses)

id: string

Message identifier (UUID)

formatuuid
created_at: string

When the message was created

formatdate-time
delivery_status: "pending" or "queued" or "sent" or 4 more

Current delivery status of a message

One of the following:
"pending"
"queued"
"sent"
"delivered"
"received"
"read"
"failed"
Deprecatedis_read: boolean

DEPRECATED: Use delivery_status == "read" instead. Whether the message has been read.

parts: array of TextPartResponse { reactions, type, value } or MediaPartResponse { id, filename, mime_type, 4 more } or LinkPartResponse { reactions, type, value }

Message parts in order (text, media, and link)

One of the following:
TextPartResponse object { reactions, type, value }

A text message part

reactions: array of Reaction { handle, is_me, type, 2 more }

Reactions on this message part

handle: ChatHandle { id, handle, joined_at, 4 more }
id: string

Unique identifier for this handle

formatuuid
handle: string

Phone number (E.164) or email address of the participant

joined_at: string

When this participant joined the chat

formatdate-time
service: ServiceType

Messaging service type. Where this names the transport a message used, it is per-message: a chat’s own service can differ from a message in it, and a conversation can fall back from RCS to SMS.

One of the following:
"SMS"
"RCS"
is_me: optional boolean

Whether this handle belongs to the sender (your phone number)

left_at: optional string

When they left (if applicable)

formatdate-time
status: optional "active" or "left" or "removed"

Participant status

One of the following:
"active"
"left"
"removed"
is_me: boolean

Whether this reaction is from the current user

Type of reaction. Standard reactions are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type “custom” with the actual emoji in the custom_emoji field.

One of the following:
"love"
"like"
"dislike"
"laugh"
"emphasize"
"question"
"custom"
id: optional string

Identifier for this reaction.

formatuuid
custom_emoji: optional string

Custom emoji if type is “custom”, null otherwise

type: "text"

Indicates this is a text message part

value: string

The text content

MediaPartResponse object { id, filename, mime_type, 4 more }

A media attachment part

id: string

Unique attachment identifier

formatuuid
filename: string

Original filename

mime_type: string

MIME type of the file

reactions: array of Reaction { handle, is_me, type, 2 more }

Reactions on this message part

handle: ChatHandle { id, handle, joined_at, 4 more }
id: string

Unique identifier for this handle

formatuuid
handle: string

Phone number (E.164) or email address of the participant

joined_at: string

When this participant joined the chat

formatdate-time
service: ServiceType

Messaging service type. Where this names the transport a message used, it is per-message: a chat’s own service can differ from a message in it, and a conversation can fall back from RCS to SMS.

One of the following:
"SMS"
"RCS"
is_me: optional boolean

Whether this handle belongs to the sender (your phone number)

left_at: optional string

When they left (if applicable)

formatdate-time
status: optional "active" or "left" or "removed"

Participant status

One of the following:
"active"
"left"
"removed"
is_me: boolean

Whether this reaction is from the current user

Type of reaction. Standard reactions are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type “custom” with the actual emoji in the custom_emoji field.

One of the following:
"love"
"like"
"dislike"
"laugh"
"emphasize"
"question"
"custom"
id: optional string

Identifier for this reaction.

formatuuid
custom_emoji: optional string

Custom emoji if type is “custom”, null otherwise

size_bytes: number

File size in bytes

type: "media"

Indicates this is a media attachment part

url: string

Presigned URL for downloading the attachment (expires in 1 hour).

formaturi
handle: ChatHandle { id, handle, joined_at, 4 more }
id: string

Unique identifier for this handle

formatuuid
handle: string

Phone number (E.164) or email address of the participant

joined_at: string

When this participant joined the chat

formatdate-time
service: ServiceType

Messaging service type. Where this names the transport a message used, it is per-message: a chat’s own service can differ from a message in it, and a conversation can fall back from RCS to SMS.

One of the following:
"SMS"
"RCS"
is_me: optional boolean

Whether this handle belongs to the sender (your phone number)

left_at: optional string

When they left (if applicable)

formatdate-time
status: optional "active" or "left" or "removed"

Participant status

One of the following:
"active"
"left"
"removed"
is_me: boolean

Whether this reaction is from the current user

Type of reaction. Standard reactions are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type “custom” with the actual emoji in the custom_emoji field.

One of the following:
"love"
"like"
"dislike"
"laugh"
"emphasize"
"question"
"custom"
id: optional string

Identifier for this reaction.

formatuuid
custom_emoji: optional string

Custom emoji if type is “custom”, null otherwise

sent_at: string

When the message was actually sent (null if still queued)

formatdate-time
delivered_at: optional string

When the message was delivered

formatdate-time
from_handle: optional ChatHandle { id, handle, joined_at, 4 more }

The sender of this message as a full handle object

id: string

Unique identifier for this handle

formatuuid
handle: string

Phone number (E.164) or email address of the participant

joined_at: string

When this participant joined the chat

formatdate-time
service: ServiceType

Messaging service type. Where this names the transport a message used, it is per-message: a chat’s own service can differ from a message in it, and a conversation can fall back from RCS to SMS.

One of the following:
"SMS"
"RCS"
is_me: optional boolean

Whether this handle belongs to the sender (your phone number)

left_at: optional string

When they left (if applicable)

formatdate-time
status: optional "active" or "left" or "removed"

Participant status

One of the following:
"active"
"left"
"removed"
reply_to: optional ReplyTo { message_id, part_index }

Indicates this message is a threaded reply to another message

message_id: string

The ID of the message to reply to

formatuuid
part_index: optional number

The specific message part to reply to (0-based index). Defaults to 0 (first part) if not provided. Use this when replying to a specific part of a multipart message.

formatint32
minimum0
service: optional ServiceType

Messaging service type. Where this names the transport a message used, it is per-message: a chat’s own service can differ from a message in it, and a conversation can fall back from RCS to SMS.

One of the following:
"SMS"
"RCS"
service: ServiceType

Messaging service type. Where this names the transport a message used, it is per-message: a chat’s own service can differ from a message in it, and a conversation can fall back from RCS to SMS.

One of the following:
"SMS"
"RCS"

Create a new chat

curl https://api.linqapp.com/api/partner/v3/chats \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \
    -d '{
          "from": "+12052535597",
          "message": {
            "parts": [
              {
                "type": "text",
                "value": "Hello! How can I help you today?"
              }
            ]
          },
          "to": [
            "+12052532136"
          ]
        }'
{
  "chat": {
    "id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9",
    "display_name": "+14155551234, +14155559876",
    "handles": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440010",
        "handle": "+14155551234",
        "joined_at": "2025-05-21T15:30:00.000Z",
        "service": "RCS",
        "is_me": true,
        "left_at": "2019-12-27T18:11:19.117Z",
        "status": "active"
      },
      {
        "id": "550e8400-e29b-41d4-a716-446655440011",
        "handle": "+14155559876",
        "joined_at": "2025-05-21T15:30:00.000Z",
        "service": "RCS",
        "is_me": false,
        "left_at": "2019-12-27T18:11:19.117Z",
        "status": "active"
      }
    ],
    "health_status": {
      "doc_url": "https://docs.linqapp.com/channel/imessage/guides/chats/chat-health#at-risk",
      "status": "AT_RISK",
      "updated_at": "2026-05-01T18:28:25Z"
    },
    "is_group": false,
    "message": {
      "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a",
      "created_at": "2025-10-23T13:07:55.019-05:00",
      "delivery_status": "pending",
      "is_read": false,
      "parts": [
        {
          "reactions": [
            {
              "handle": {
                "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a",
                "handle": "+15551234567",
                "joined_at": "2025-05-21T15:30:00.000-05:00",
                "service": "RCS",
                "is_me": false,
                "left_at": "2019-12-27T18:11:19.117Z",
                "status": "active"
              },
              "is_me": false,
              "type": "love",
              "id": "9f8b1c2d-3e4f-5061-7283-94a5b6c7d8e9",
              "custom_emoji": null
            }
          ],
          "type": "text",
          "value": "Hello!"
        }
      ],
      "sent_at": null,
      "delivered_at": null,
      "from_handle": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "handle": "+15551234567",
        "joined_at": "2025-05-21T15:30:00.000-05:00",
        "service": "RCS",
        "is_me": false,
        "left_at": "2019-12-27T18:11:19.117Z",
        "status": "active"
      },
      "reply_to": {
        "message_id": "550e8400-e29b-41d4-a716-446655440000",
        "part_index": 0
      },
      "service": "RCS"
    },
    "service": "RCS"
  }
}
{
  "error": {
    "status": 400,
    "code": 1002,
    "message": "Phone number must be in E.164 format",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/1xxx/1002/"
  },
  "success": false
}
{
  "error": {
    "status": 401,
    "code": 2004,
    "message": "Unauthorized - missing or invalid authentication token",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/2xxx/2004/"
  },
  "success": false
}
{
  "error": {
    "status": 403,
    "code": 2005,
    "message": "Access denied - insufficient permissions for this resource",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/2xxx/2005/"
  },
  "success": false
}
{
  "error": {
    "status": 500,
    "code": 3006,
    "message": "Internal server error",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/3xxx/3006/"
  },
  "success": false
}
Returns Examples
{
  "chat": {
    "id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9",
    "display_name": "+14155551234, +14155559876",
    "handles": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440010",
        "handle": "+14155551234",
        "joined_at": "2025-05-21T15:30:00.000Z",
        "service": "RCS",
        "is_me": true,
        "left_at": "2019-12-27T18:11:19.117Z",
        "status": "active"
      },
      {
        "id": "550e8400-e29b-41d4-a716-446655440011",
        "handle": "+14155559876",
        "joined_at": "2025-05-21T15:30:00.000Z",
        "service": "RCS",
        "is_me": false,
        "left_at": "2019-12-27T18:11:19.117Z",
        "status": "active"
      }
    ],
    "health_status": {
      "doc_url": "https://docs.linqapp.com/channel/imessage/guides/chats/chat-health#at-risk",
      "status": "AT_RISK",
      "updated_at": "2026-05-01T18:28:25Z"
    },
    "is_group": false,
    "message": {
      "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a",
      "created_at": "2025-10-23T13:07:55.019-05:00",
      "delivery_status": "pending",
      "is_read": false,
      "parts": [
        {
          "reactions": [
            {
              "handle": {
                "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a",
                "handle": "+15551234567",
                "joined_at": "2025-05-21T15:30:00.000-05:00",
                "service": "RCS",
                "is_me": false,
                "left_at": "2019-12-27T18:11:19.117Z",
                "status": "active"
              },
              "is_me": false,
              "type": "love",
              "id": "9f8b1c2d-3e4f-5061-7283-94a5b6c7d8e9",
              "custom_emoji": null
            }
          ],
          "type": "text",
          "value": "Hello!"
        }
      ],
      "sent_at": null,
      "delivered_at": null,
      "from_handle": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "handle": "+15551234567",
        "joined_at": "2025-05-21T15:30:00.000-05:00",
        "service": "RCS",
        "is_me": false,
        "left_at": "2019-12-27T18:11:19.117Z",
        "status": "active"
      },
      "reply_to": {
        "message_id": "550e8400-e29b-41d4-a716-446655440000",
        "part_index": 0
      },
      "service": "RCS"
    },
    "service": "RCS"
  }
}
{
  "error": {
    "status": 400,
    "code": 1002,
    "message": "Phone number must be in E.164 format",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/1xxx/1002/"
  },
  "success": false
}
{
  "error": {
    "status": 401,
    "code": 2004,
    "message": "Unauthorized - missing or invalid authentication token",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/2xxx/2004/"
  },
  "success": false
}
{
  "error": {
    "status": 403,
    "code": 2005,
    "message": "Access denied - insufficient permissions for this resource",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/2xxx/2005/"
  },
  "success": false
}
{
  "error": {
    "status": 500,
    "code": 3006,
    "message": "Internal server error",
    "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/3xxx/3006/"
  },
  "success": false
}