# Linq Partner API — Full Documentation > Integrate iMessage, RCS, and SMS messaging directly into your applications. This file is the complete documentation for Linq Partner API, concatenated for LLM ingestion. Each section below corresponds to one page of the site and is preceded by its canonical URL. The companion file [llms.txt](https://docs.linqapp.com/llms.txt) is a shorter index pointing at individual pages, and the OpenAPI specification at https://cdn.linqapp.com/openapi/linq-api-v3.yaml is the machine-readable contract for every endpoint. **Pages included:** 177 --- # Introduction URL: https://docs.linqapp.com/ Still on V2? Migrate to V3 and unlock delivery receipts, trace IDs, message effects, custom reactions, and more. See the [V2 → V3 migration guide](/guides/resources/migration-v2-to-v3/index.md). The Linq Partner API is a RESTful API at `https://api.linqapp.com/api/partner/v3` that provides programmatic access to iMessage, RCS, and SMS messaging infrastructure. > **New to Linq?** Start with the [Quickstart](/getting-started/quickstart/index.md) to send your first message in under 5 minutes, or explore the [API Reference](/api/index.md) for the complete endpoint specification. Building with an AI agent? Feed these docs straight to your LLM or coding agent: [`llms-full.txt`](https://docs.linqapp.com/llms-full.txt) is the complete documentation in one file, and [`llms.txt`](https://docs.linqapp.com/llms.txt) is a concise index. More in [LLM-friendly docs](/guides/resources/faq#llm-friendly-docs/index.md). ## Prerequisites To use the Linq Partner API, you’ll need: - A **bearer token** provisioned by your Linq representative - One or more **phone numbers** assigned to your account - An endpoint URL for receiving **webhooks** (optional but recommended) For step-by-step setup, see [Quickstart](/getting-started/quickstart/index.md). ## What you can build The Linq Partner API supports a wide range of messaging use cases: - **Customer support** — Route conversations, send rich media, and integrate with your support stack - **Sales engagement** — Personalized outreach with read receipts and delivery tracking - **Appointment reminders** — Automated notifications with confirmation workflows - **Order notifications** — Shipping updates, delivery confirmations, and feedback collection - **AI-powered agents** — Build conversational AI that communicates over iMessage, RCS, and SMS See [open-source examples](/guides/resources/example/index.md) for real-world implementations. ## Key capabilities | Feature | Description | | ---------------------- | ---------------------------------------------------------------------------------------------- | | **Unified messaging** | Send via iMessage, RCS, or SMS from a single API with automatic or explicit protocol selection | | **Rich media** | Share images, videos, documents, voice memos, and contact cards up to 100MB | | **Group chats** | Create and manage group conversations with participant controls | | **Message threading** | Reply to specific messages within a conversation | | **Reactions** | Add built-in tapbacks or any custom Unicode emoji to messages | | **Message effects** | Confetti, fireworks, slam, gentle, invisible ink, and more (iMessage) | | **Typing indicators** | Show and receive real-time typing status | | **Real-time webhooks** | Instant notifications for delivery, read receipts, reactions, and incoming messages | | **Built-in debugging** | W3C trace IDs on every request for end-to-end observability | ## Authentication All requests require a bearer token in the `Authorization` header: ``` Authorization: Bearer YOUR_TOKEN ``` For details on token management and security best practices, see [Authentication](/getting-started/authentication/index.md). ## Quick example Send a message with a single API call: Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "from": "+12223334444", "to": ["+15556667777"], "message": { "parts": [ { "type": "text", "value": "Hello from Linq!" } ] } }' ``` ## Handle formats - **Phone numbers** must be in E.164 format: `+12223334444` - **Email addresses** use standard format: `user@example.com` - No spaces, dashes, or parentheses in phone numbers ## Next steps Quickstart Send your first message in under 5 minutes. [Get started →](/getting-started/quickstart/index.md) Sending Messages Text, media, threading, effects, and protocol selection. [Learn more →](/guides/messaging/sending-messages/index.md) Webhooks Real-time event notifications with signature verification. [Set up webhooks →](/guides/webhooks/index.md) API Reference Complete endpoint specification with request and response schemas. [View reference →](/api/index.md) Client SDKs Official TypeScript and Python SDKs with type safety and automatic retries. [Install SDKs →](/getting-started/sdks/index.md) Error Codes Complete error reference with troubleshooting guidance. [View errors →](/error/index.md) --- # Authentication URL: https://docs.linqapp.com/getting-started/authentication/ All requests to the Linq Partner API require authentication via a bearer token in the `Authorization` header. The official [SDKs](/getting-started/sdks/index.md) handle authentication automatically. ## Bearer token authentication Include your API token in every request: ``` Authorization: Bearer YOUR_TOKEN ``` Your token determines: - Which **phone numbers** you can send from - Which **data** you can access (chats, messages, attachments) - Your **rate limits** and daily message quotas ## Getting your token Generate a token yourself from the Linq dashboard: 1. Go to [dashboard.linqapp.com/api-tooling](https://dashboard.linqapp.com/api-tooling). 2. **API → Overview** 3. Click **Generate new token**. ## Making authenticated requests Terminal window ``` curl https://api.linqapp.com/api/partner/v3/chats \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" ``` ``` import LinqAPIV3 from '@linqapp/sdk'; const client = new LinqAPIV3({ apiKey: process.env.LINQ_API_KEY, // reads LINQ_API_V3_API_KEY by default }); ``` ``` import os from linq import LinqAPIV3 client = LinqAPIV3(api_key=os.environ["LINQ_API_KEY"]) ``` ## Token security best practices - **Use environment variables** — Never hardcode tokens in source code - **Never commit tokens** — Add `.env` to `.gitignore` - **Rotate regularly** — Request new tokens periodically from your Linq representative - **Limit access** — Only share tokens with team members who need them - **Use separate tokens** — Use different tokens for development and production ## Error responses | Status | Code | Description | | ------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- | | `401` | [2004](/error/codes/2xxx/2004/index.md) | Missing or invalid bearer token. Check that your `Authorization` header is correctly formatted. | | `403` | [2005](/error/codes/2xxx/2005/index.md) | Valid token but insufficient permissions for this resource. | | `403` | [2006](/error/codes/2xxx/2006/index.md) | You cannot send from this phone number. Verify the phone is assigned to your account. | | `429` | [1007](/error/codes/1xxx/1007/index.md) | Rate limit exceeded. See [Rate Limits](/guides/platform/rate-limits/index.md). | See [Error Codes](/error/index.md) for the complete error reference and the [API Reference](/api/index.md) for endpoint specifications. --- # Best Practices URL: https://docs.linqapp.com/getting-started/best-practices/ Follow this flow for every new contact to maintain healthy iMessage conversations and optimal deliverability. ## Messaging flow 1. **Set up your contact card** — configure your name and photo via the [Contact Cards](/guides/contact-cards/index.md) endpoints for your numbers. This is a one-time setup per number. 2. **Receive an inbound message first** — wait for the recipient to initiate or respond before sharing your card. See [Sending Messages](/guides/messaging/sending-messages/index.md). 3. **Send an outbound message** — there must be at least one outbound message in the chat before sharing. 4. **Share your contact card** — call the share endpoint so the recipient is prompted to save your name and photo. See [Sharing Contact Card](/guides/chats/share-contact-card/index.md). There’s no confirmation the user saved it, so call the share endpoint once per day after the first outbound activity on that chat — this keeps giving them the option if they dismissed it. 5. **Elicit 3+ responses early** — aim to get at least three replies from a new user as soon as possible to keep the conversation healthy. 6. **Maintain a 1:2 inbound:outbound ratio** — a message reciprocity ratio of 1 inbound for every 2 outbound messages leads to optimal deliverability. 7. **Honor opt-out language** — immediately stop messaging any user who sends stop, unsubscribe, or expresses negative sentiment. ## Tips DO - Design for inbound-first messaging - Space messages naturally - Make interactions conversational - Share contact cards early - Round-robin new users across lines to balance load DON'T - Exceed 7,000 messages/day per line - Include links or media in your first message - Keep fallback lines dormant - Bombard non-responders - Segment Android users to separate lines - Use iMessage for cold outreach --- # Key Concepts URL: https://docs.linqapp.com/getting-started/key-concepts/ This page is the shared vocabulary every other guide builds on. Read it once, then skim the linked guides for depth. ## Handles A **handle** is an addressable endpoint — a phone number in E.164 format (`+14155551234`) or an email address (`user@example.com`). Handles appear on both sides of a conversation: the `from` handle (one of your provisioned numbers) and the recipient handles in `to`. Handles carry metadata: - `service` — `iMessage`, `RCS`, or `SMS` (the protocol the handle is reachable on) - `is_me` — whether this handle is yours - `status` — for group participants: `active`, `left`, or `removed` See [Protocol Selection](/guides/messaging/protocol-selection/index.md) for how handles map to delivery services. ## Phone numbers Your account is provisioned with one or more **phone numbers** (also called “lines”). Every outbound message is sent `from` one of these numbers. Numbers have a `status` (`ACTIVE` or `FLAGGED`) that is surfaced via the [`phone_number.status_updated`](/guides/webhooks/events#phone-number-events/index.md) webhook. Contact your Linq representative to provision additional numbers. ## Chats A **chat** is the container for a conversation between one of your numbers and one or more recipient handles. Everything else — messages, reactions, typing indicators, participants — hangs off a chat. - **Direct message chat** — exactly one recipient - **Group chat** — two or more recipients (maximum **31** handles in `to`); requires iMessage or RCS. MMS group chats are also supported (see [Group Chats](/guides/chats/group-chats/index.md)) Chats are created via `POST /v3/chats` with an initial message. The same endpoint handles both direct and group conversations — the shape of `to` determines which you get. ## Messages and parts A **message** belongs to a chat and is composed of an ordered `parts` array. Each part is one of: - `text` — a string value (up to 10,000 characters) - `media` — an image, video, document, or audio file, referenced by `url` or pre-uploaded `attachment_id` - `link` — a URL (up to 2,048 characters) that renders as a rich preview; must be the only part in its message See [Sending Messages](/guides/messaging/sending-messages/index.md) and [Attachments](/guides/messaging/attachments/index.md) for the full part schemas and limits. ## Attachments An **attachment** is a pre-uploaded file (up to 100 MB) that you can reference by `attachment_id` across multiple messages. Attachments never expire — reuse them freely. Files under 10 MB can skip pre-upload and be inlined via a public HTTPS `url`. See [Attachments](/guides/messaging/attachments/index.md). ## Services and protocols Linq unifies three messaging services behind one API: | Service | Carrier-of-record | Notes | | ------------ | ----------------- | ------------------------------------------------------------------------------ | | **iMessage** | Apple | Rich features (effects, tapbacks, read receipts, typing, editing, decorations) | | **RCS** | Carrier | Rich features on Android (reactions, read receipts, typing, group chats) | | **SMS/MMS** | Carrier | Baseline text-and-MMS fallback with the widest reach | The API picks the best service automatically, or you can pin one with `preferred_service`. See [Protocol Selection](/guides/messaging/protocol-selection/index.md) and use the [capability-check endpoints](/guides/messaging/protocol-selection#checking-capability/index.md) to probe a handle before you send. ## Webhooks **Webhooks** are your real-time feed of what happens after the API returns. You subscribe to a set of event types at a URL you own, and Linq POSTs envelopes with a stable `event_id`, a request-wide `trace_id`, and an event-specific `data` payload. - At-least-once delivery — deduplicate with `event_id` - HMAC-SHA256 signed with your webhook secret - Versioned via `?version=YYYY-MM-DD` in the subscription URL See [Webhooks](/guides/webhooks/index.md) for signature verification, the [supported event types](/guides/webhooks/events/index.md), and retry policy. ## Trace IDs Every API response and webhook payload carries a **trace ID** — the W3C Trace Context `trace-id` (32 hex chars). It’s the single handle you use to correlate an API call, its asynchronous delivery lifecycle, and any related support ticket. See [Debugging](/guides/platform/debugging/index.md) for the full correlation pattern. ## Idempotency Message sends accept an `idempotency_key` field in the message body (max 255 chars). Send the same key on a retry and Linq returns the original response instead of sending twice. See [Sending Messages](/guides/messaging/sending-messages#idempotency/index.md). ## Rate limits Two independent limits apply: - **Recommended daily volume:** 7,000 combined inbound+outbound messages per line per day - **Per-pair burst limit:** 30 messages per 60-second window per unique sender–recipient pair - **Sandbox accounts:** 100 messages per day Exceeding a limit returns HTTP 429 with error code `1007`. See [Rate Limits](/guides/platform/rate-limits/index.md). ## Errors All errors return a JSON body with a numeric `code`, human-readable `message`, HTTP `status`, and a `doc_url` linking directly to the error reference page. A top-level `trace_id` is also included for debugging. Codes are grouped by range (1xxx client, 2xxx resource, 3xxx server, 4xxx delivery, 5xxx file). See [Error Codes](/error/index.md). ## Next steps - [Quickstart](/getting-started/quickstart/index.md) — send your first message - [Sending Messages](/guides/messaging/sending-messages/index.md) — parts, effects, decorations, editing - [Webhooks](/guides/webhooks/index.md) — real-time events - [API Reference](/api/index.md) — complete endpoint specification --- # Quickstart URL: https://docs.linqapp.com/getting-started/quickstart/ This guide walks you through sending your first message with the Linq Partner API. Using an AI agent? Point your LLM or coding agent at [`llms-full.txt`](https://docs.linqapp.com/llms-full.txt) — the complete docs in a single file — so it can write Linq integrations for you. See [LLM-friendly docs](/guides/resources/faq#llm-friendly-docs/index.md). ## Prerequisites Before you begin, make sure you have: - A **bearer token** from your Linq representative - At least one **phone number** provisioned on your account - A recipient phone number in **E.164 format** (e.g., `+15556667777`) ## 1. Install an SDK (optional) - [TypeScript](#tab-panel-4) - [Python](#tab-panel-5) - [cURL](#tab-panel-6) Terminal window ``` npm install @linqapp/sdk ``` Terminal window ``` pip install linq-python ``` No installation needed — use `curl` from your terminal. ## 2. Send your first message Create a chat and send a message in a single request: - [cURL](#tab-panel-7) - [TypeScript](#tab-panel-8) - [Python](#tab-panel-9) 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": "+12223334444", "to": ["+15556667777"], "message": { "parts": [ { "type": "text", "value": "Hello from Linq!" } ] } }' ``` ``` import LinqAPIV3 from '@linqapp/sdk'; const client = new LinqAPIV3({ apiKey: process.env.LINQ_API_KEY, }); const chat = await client.chats.create({ from: '+12223334444', to: ['+15556667777'], message: { parts: [ { type: 'text', value: 'Hello from Linq!' } ], }, }); console.log('Chat created:', chat.id); console.log('Message ID:', chat.last_message?.id); ``` ``` import os from linq import LinqAPIV3 client = LinqAPIV3(api_key=os.environ["LINQ_API_KEY"]) chat = client.chats.create( from_="+12223334444", to=["+15556667777"], message={ "parts": [ {"type": "text", "value": "Hello from Linq!"} ] }, ) print(f"Chat created: {chat.id}") print(f"Message ID: {chat.last_message.id}") ``` You’ll receive a response with the chat details and message status: ``` { "id": "550e8400-e29b-41d4-a716-446655440000", "is_group": false, "last_message": { "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "parts": [ { "type": "text", "value": "Hello from Linq!" } ], "sent_at": "2026-02-05T19:52:17.219Z", "service": "iMessage" } } ``` ## 3. Send a follow-up message Once you have a chat ID, send additional messages to the same conversation: - [cURL](#tab-panel-10) - [TypeScript](#tab-panel-11) - [Python](#tab-panel-12) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chat_id}/messages \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "parts": [ { "type": "text", "value": "Following up!" } ] }' ``` ``` const message = await client.chats.messages.send(chat.id, { parts: [ { type: 'text', value: 'Following up!' } ], }); ``` ``` message = client.chats.messages.send( chat.id, parts=[{"type": "text", "value": "Following up!"}], ) ``` ## 4. Set up webhooks To receive real-time notifications when messages are delivered, read, or received, create a webhook subscription: - [cURL](#tab-panel-13) - [TypeScript](#tab-panel-14) - [Python](#tab-panel-15) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/webhook-subscriptions \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "target_url": "https://your-server.com/webhook?version=2026-02-03", "subscribed_events": [ "message.sent", "message.received", "message.delivered", "message.read", "message.failed" ] }' ``` ``` const subscription = await client.webhookSubscriptions.create({ target_url: 'https://your-server.com/webhook?version=2026-02-03', subscribed_events: [ 'message.sent', 'message.received', 'message.delivered', 'message.read', 'message.failed', ], }); ``` ``` subscription = client.webhook_subscriptions.create( target_url="https://your-server.com/webhook?version=2026-02-03", subscribed_events=[ "message.sent", "message.received", "message.delivered", "message.read", "message.failed", ], ) ``` > **Tip:** If no version is specified, the subscription uses the latest available version at creation time. Pass `?version=YYYY-MM-DD` explicitly to pin a specific payload format. See [Webhooks → Versioning](/guides/webhooks#webhook-versioning/index.md) and [Signature verification](/guides/webhooks#signature-verification/index.md). ## Next steps - [Sending Messages](/guides/messaging/sending-messages/index.md) — Text, media, threading, and effects - [Attachments](/guides/messaging/attachments/index.md) — Send images, videos, and documents - [Webhooks](/guides/webhooks/index.md) — Signature verification and event handling - [Group Chats](/guides/chats/group-chats/index.md) — Multi-participant conversations - [Error Codes](/error/index.md) — Troubleshooting API errors - [API Reference](/api/index.md) — Complete endpoint specification --- # Client SDKs URL: https://docs.linqapp.com/getting-started/sdks/ Linq provides official SDKs that simplify API integration by handling [authentication](/getting-started/authentication/index.md), request formatting, [error handling](/error/index.md), and type safety. ## TypeScript / Node.js Terminal window ``` npm install @linqapp/sdk ``` ``` import LinqAPIV3 from '@linqapp/sdk'; const client = new LinqAPIV3({ apiKey: process.env.LINQ_API_KEY, }); // Send a message const chat = await client.chats.create({ from: '+12223334444', to: ['+15556667777'], message: { parts: [{ type: 'text', value: 'Hello from Linq!' }], }, }); ``` ## Python Terminal window ``` pip install linq-python ``` ``` import os from linq import LinqAPIV3 client = LinqAPIV3(api_key=os.environ["LINQ_API_KEY"]) # Send a message chat = client.chats.create( from_="+12223334444", to=["+15556667777"], message={ "parts": [{"type": "text", "value": "Hello from Linq!"}] }, ) ``` ## Go Terminal window ``` go get -u github.com/linq-team/linq-go ``` ``` package main import ( "context" "fmt" "github.com/linq-team/linq-go" // imported as linqgo "github.com/linq-team/linq-go/option" ) func main() { client := linqgo.NewClient( option.WithAPIKey("My API Key"), // defaults to os.LookupEnv("LINQ_API_V3_API_KEY") ) chat, err := client.Chats.New(context.TODO(), linqgo.ChatNewParams{ From: "+12223334444", To: []string{"+15556667777"}, Message: linqgo.MessageContentParam{ Parts: []linqgo.MessageContentPartUnionParam{{ OfText: &linqgo.TextPartParam{ Type: linqgo.TextPartTypeText, Value: "Hello from Linq!", }, }}, }, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", chat.Chat) } ``` Requires Go 1.22+. ## SDK features All official SDKs include: | Feature | Description | | ---------------------------- | ----------------------------------------------------------------------------------------------------------- | | **Automatic authentication** | Reads your API key from environment variables or constructor | | **Type safety** | Full type definitions for all request and response objects | | **Automatic retries** | Retries failed requests with exponential backoff (see [Rate Limits](/guides/platform/rate-limits/index.md)) | | **Error handling** | Typed error classes with status codes and error details | | **Idempotency** | Built-in idempotency key support for safe retries | | **Pagination** | Helpers for iterating over paginated responses | ## Configuration Both SDKs accept the same configuration options: ``` const client = new LinqAPIV3({ apiKey: 'your-api-key', // Default: process.env.LINQ_API_V3_API_KEY baseURL: 'https://custom-url.com', // Default: https://api.linqapp.com/api/partner timeout: 30000, // Request timeout in ms (default: 60000) maxRetries: 3, // Max retry attempts (default: 2) }); ``` ``` client = LinqAPIV3( api_key="your-api-key", # Default: os.environ["LINQ_API_V3_API_KEY"] base_url="https://custom-url.com", # Default: https://api.linqapp.com/api/partner timeout=30.0, # Request timeout in seconds (default: 60) max_retries=3, # Max retry attempts (default: 2) ) ``` ## Using the API directly You can also call the API directly with any HTTP client. All endpoints accept and return JSON: 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": "+12223334444", "to": ["+15556667777"], "message": { "parts": [{ "type": "text", "value": "Hello!" }] } }' ``` See the [API Reference](/api/index.md) for the complete endpoint specification. --- # Capability Checks URL: https://docs.linqapp.com/guides/chats/capability-checks/ Check whether a recipient address supports iMessage or RCS before sending a message. Use capability checks to pick the right [protocol](/guides/messaging/protocol-selection/index.md), to gate rich-only features (effects, decorations), or to fall back gracefully to SMS when a richer service isn’t available. You do not need to capability-check every send — the API selects the best available service automatically when `preferred_service` is omitted. Capability checks matter when you want to make product decisions *before* calling `POST /v3/chats`. ## Check iMessage capability POST the recipient handle to `/v3/capability/check_imessage`. The response includes an `available` boolean — branch on it to decide whether to use iMessage-only features. See the [Check iMessage API reference](/api/resources/capability/methods/check_imessage/index.md). - [cURL](#tab-panel-16) - [TypeScript](#tab-panel-17) - [Python](#tab-panel-18) - [Go](#tab-panel-19) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/capability/check_imessage \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.capability.checkiMessage(); ``` ``` client.capability.checki_message() ``` ``` client.Capability.CheckiMessage(context.TODO()) ``` ## Check RCS capability Same shape against `/v3/capability/check_rcs`. See the [Check RCS API reference](/api/resources/capability/methods/check_rcs/index.md). - [cURL](#tab-panel-20) - [TypeScript](#tab-panel-21) - [Python](#tab-panel-22) - [Go](#tab-panel-23) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/capability/check_rcs \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.capability.checkRCS(); ``` ``` client.capability.check_rcs() ``` ``` client.Capability.CheckRCS(context.TODO()) ``` ## Request fields | Field | Required | Description | | --------- | -------- | ------------------------------------------------------------------------------------------ | | `address` | Yes | Recipient handle — phone number (E.164) or email address for iMessage | | `from` | No | One of your provisioned numbers to run the check from — pins the result to a specific line | ## When to use each service | If the check says… | Do this | | ------------------ | ------------------------------------------------------------------------------------------------ | | iMessage capable | Send with `preferred_service: "iMessage"` to unlock effects, decorations, read receipts, editing | | RCS capable | Send with `preferred_service: "RCS"` for rich features on Android | | Neither | Fall back to SMS/MMS, or let automatic selection pick SMS | ## Caching results Capability is a property of the recipient’s device and account, not a static attribute of the phone number — a user switching from iPhone to Android, or disabling iMessage, changes the answer. Cache results only briefly (minutes, not days), and refresh on any `message.failed` event whose error suggests a service mismatch. ## Rate limits Capability check endpoints are rate-limited to prevent abuse (e.g. using them to enumerate which numbers are on iMessage). Excessive calls return HTTP `429` with a `Retry-After` header and [error code `1007`](/error/codes/1xxx/1007/index.md). Caching results as described above is the easiest way to stay under the limit. See [Rate Limits](/guides/platform/rate-limits#capability-check-rate-limit/index.md) for the full response shape and retry handling. ## Related - [Protocol Selection](/guides/messaging/protocol-selection/index.md) — how services are picked and what each supports - [Sending Messages](/guides/messaging/sending-messages/index.md) — using `preferred_service` on send - [Key Concepts: Handles](/getting-started/key-concepts#handles/index.md) — the handle model - [Rate Limits](/guides/platform/rate-limits#capability-check-rate-limit/index.md) — handling `429` responses from capability endpoints - [API Reference: Capability](/api/resources/capability/index.md) — complete endpoint specification --- # Chat Health URL: https://docs.linqapp.com/guides/chats/chat-health/ Note This feature is currently in beta and may be inaccurate at times. We are actively improving our health score models. Expect additional engagement and intent signals as the scoring gets smarter. Every chat carries a `health_status` — the field you can check **before sending** to decide what to do with the next outbound on that conversation. While there isn’t a direct link between deliverability and chat health, it is a prediction and analysis of messaging behavior. You’ll see it on every chat-related webhook event and on every chat read. Treat it as a pre-send gate, not as much a report. ## Engagement is a strong signal If you take one thing from this page: **two-way engagement is among the strongest signals we use**, and it rolls directly into your [line’s reputation](/guides/phone-numbers/phone-reputation/index.md). Continuing to send messages into silence is one of the most common reasons a conversation — and its line — slides to `AT_RISK` or even `CRITICAL`. So: - **Send messages built to get a reply.** Lead with a question or a clear, relevant prompt. - **Let replies set your pace, and back off when they stop.** See [How many messages should I send?](#how-many-messages) for the cadence and back-off ladder. ## How to use it We recommend you do whatever is best for your messaging use case, but one example could be to cache the most recent `health_status.status` from your webhook stream and check it as a pre-flight before queueing each outbound: ``` switch (chat.health_status.status) { case 'HEALTHY': send(message); break; case 'AT_RISK': checkReplyRate(); break; case 'CRITICAL': pause(chat); break; case 'OPTED_OUT': skip(chat); // terminal — never resume sending } ``` Acting on the status before each send is what turns the signal into delivery improvement — ultimately leading to a healthier line. ## Statuses | `status` | What it means | What to do | | ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | [`HEALTHY`](#healthy) | Healthy conversation. | Send normally. | | [`AT_RISK`](#at-risk) | Poor engagement signals, which may lead to worsening health if current messaging patterns continue. | Slow outbound on this chat and check your reply rate. | | [`CRITICAL`](#critical) | Strong signals that messages aren’t landing well. | Pause messaging on this chat until healthy. | | [`OPTED_OUT`](#opted-out) | The recipient asked you to stop. | Terminal. Immediately stop messaging this chat. | ### HEALTHY The chat looks like a normal conversation. Replies are landing, delivery signals look good, and no opt-out language has been detected. No action needed. ### []()AT\_RISK One or more soft signals suggest this chat is heading in the wrong direction. Common drivers: - **Low engagement.** The ratio of recipient replies to your sends is low. `AT_RISK` is a *warning*, not a hard stop. What to do: - **Slow outbound and vary your content.** Reduce send frequency; repeated near-identical messages amplify negative signal. - **Back off if replies have stopped.** Don’t hold the same cadence into silence — see [How many messages should I send?](#how-many-messages). Watch for the chat moving back to `HEALTHY` (good) or down to `CRITICAL` (act fast). ### CRITICAL Strong signals that messages on this chat aren’t reaching the recipient the way you expect. Continuing to send is unlikely to help and may make the situation worse for the broader line. Recommended action: Pause this chat. Re-engage only after chat becomes healthy again. ### []()OPTED\_OUT The recipient sent an opt-out keyword on this chat. This is terminal: regardless of any other signals, do not send further outbound messages on this chat. The full set of opt-out keywords: `STOP`, `UNSUBSCRIBE`, `OPTOUT`, `CANCEL`, `END`, `QUIT` Currently, matching is exact and case-sensitive against the inbound message. If the recipient later sends the opt-in keyword `OPTIN` (same matching rules), the opt-out clears and the chat moves back to whichever health bucket current signals indicate. ## Chat health and phone reputation Chat health is a leading signal for your line’s [reputation](/guides/phone-numbers/phone-reputation/index.md): the health of the conversations on a line rolls up into the line’s overall reputation. Many `AT_RISK` or `CRITICAL` chats on a single line increase the chance that the line will be flagged by our systems or carriers. That said, **chat health is not the only thing that affects line reputation.** We are continuously improving our models to create a healthy ecosystem. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation/index.md) for the line-level view. ## Tips - New chats start as `HEALTHY` and move to `AT_RISK`, `CRITICAL`, or `OPTED_OUT` as signals warrant. - `updated_at` tells you when the status last changed; use it to detect rapid status drops in your dashboards. - Switch on `health_status` according to the use cases above. ## FAQ **[]()How many messages should I send to keep a chat healthy?** Let replies set the pace. A back-and-forth conversation can sustain a normal cadence; a one-sided one can’t. As a rule of thumb, keep at least **2–3 recipient replies** flowing for the volume you send, and don’t send many messages a week into a chat that isn’t replying. When a recipient goes quiet, slow down and eventually stop — sending harder into silence is one of the fastest ways to push a chat to `AT_RISK` or `CRITICAL`. Use an escalating back-off: 1. **No reply?** Wait about a day, then send **one** follow-up. 2. **Still no reply?** Wait a few days, then send **one** more. 3. **Still nothing?** Send a final message that gives the recipient an easy way out — for example, asking whether they’d like to stop receiving messages — then **halt all outbound to that recipient.** 4. **Wait for a reply before sending again.** When they respond, read it carefully: a clear “stop” (or an [opt-out keyword](#opted-out)) means you’re done; genuine interest means you can resume at a normal, reply-paced cadence. **Does chat health read message content or store any PII?** No. Evaluating health status runs on anonymous, aggregate signals — message volume, sends vs. receives, response cadence, and similar metadata. Inbound text is scanned at runtime (JIT) to detect opt-out language for [`OPTED_OUT`](#opted-out) signals, but message content is never collected, stored, or retained. No PII is persisted. --- # Group Chats URL: https://docs.linqapp.com/guides/chats/group-chats/ Group chats allow you to create conversations with three or more participants. They support display names, icons, and participant management. See the [Chats API Reference](/api/resources/chats/index.md) for the full endpoint specification. > **Note:** [Typing indicators](/guides/chats/typing-indicators/index.md), delivery receipts, and read receipts are **not supported** in group chats. These features only work in one-to-one conversations. ## Creating a group chat Create a group by sending a message to multiple recipients: 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": "+12223334444", "to": ["+15556667777", "+18889990000"], "message": { "parts": [ { "type": "text", "value": "Welcome to the group!" } ] } }' ``` A few constraints on this request: - 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](/guides/messaging/rich-link-previews/index.md) using the returned chat ID. - A chat’s `to` array supports a maximum of **31 recipients**. SMS/MMS group chats are additionally limited by carrier settings — most carriers cap group texts at around **20 participants**, and some restrict this to as few as **10**. Plan for the stricter of the two when delivery falls back to SMS/MMS. ## Updating group details Set a display name and icon for the group. Only available for group conversations: - [cURL](#tab-panel-24) - [TypeScript](#tab-panel-25) - [Python](#tab-panel-26) - [Go](#tab-panel-27) Terminal window ``` curl -X PUT https://api.linqapp.com/api/partner/v3/chats/{chatId} \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "display_name": "Team Discussion", "group_chat_icon": "https://example.com/group-icon.png" }' ``` ``` await client.chats.update({chatId}, { display_name: "Team Discussion", group_chat_icon: "https://example.com/group-icon.png", }); ``` ``` client.chats.update( {chat_id}, display_name="Team Discussion", group_chat_icon="https://example.com/group-icon.png", ) ``` ``` client.Chats.Update(context.TODO(), {chatId}, linq.ChatUpdateParams{ DisplayName: linq.F("Team Discussion"), GroupChatIcon: linq.F("https://example.com/group-icon.png"), }) ``` > **Note:** `group_chat_icon` is a publicly accessible HTTPS URL to the icon image. Updating display names and icons only works for group chats ([error code `1006`](/error/codes/1xxx/1006/index.md) is returned for direct messages). See the [Update Chat](/api/resources/chats/methods/update/index.md) endpoint for the full request schema. ## Managing participants > **Note:** Managing participants is currently supported for iMessage group chats only. ### Adding a participant Add a phone number or email to an existing group chat. See the [Add Participant API reference](/api/resources/chats/subresources/participants/methods/add/index.md). ### Removing a participant Remove a participant by handle. See the [Remove Participant API reference](/api/resources/chats/subresources/participants/methods/remove/index.md). > **Important:** Groups must always have at least 3 members. You cannot remove a participant if it would drop below this minimum. ## Leaving a group chat Remove your own phone number from a group conversation. See the [Leave Chat API reference](/api/resources/chats/methods/leave_chat/index.md). > **Important:** Groups must always have at least 3 members. You cannot leave a group if it would drop below this minimum. Once you leave, you can no longer access the chat unless an active participant adds you back. Recreating a chat with the same set of participants will create a new, separate chat. A [`participant.removed`](/api/resources/webhooks/index.md) webhook fires once the leave has been processed. For the webhook events that fire on chat creation, group updates, and participant changes, see [Webhook Events → Chat events](/guides/webhooks/events#chat-events/index.md). --- # Chats URL: https://docs.linqapp.com/guides/chats/ A Chat is a conversation thread with one or more participants. To begin a chat, you must create a Chat with at least one recipient handle. Including multiple handles creates a group chat. When creating a chat, the `from` field specifies which of your authorized phone numbers the message originates from. Your authentication token grants access to one or more phone numbers, but the `from` field determines the actual sender. **Handle Format:** - Handles can be phone numbers or email addresses - Phone numbers MUST be in E.164 format (starting with +) - Phone format: `+[country code][subscriber number]` - Example phone: `+12223334444` (US), `+442071234567` (UK), `+81312345678` (Japan) - Example email: `user@example.com` - No spaces, dashes, or parentheses in phone numbers ## Create a chat Chats are created by sending the first message. See [Starting a conversation](/guides/messaging/sending-messages#starting-a-conversation/index.md) in the Sending Messages guide for the full example, or the [Create Chat API reference](/api/resources/chats/methods/create/index.md). ## List chats List every chat the authenticated partner has access to, optionally filtered by sender number (`from`) or participant (`to`). Results are paginated — pass the previous response’s `next_cursor` back as `cursor` until it returns `null`. Default page size is 20, max is 100. See the [List Chats API reference](/api/resources/chats/methods/list_chats/index.md). ## Retrieve a chat Fetch a single chat by ID. The response includes participants, conversation protocol, [chat health](/guides/chats/chat-health/index.md), and more. See the [Retrieve Chat API reference](/api/resources/chats/methods/retrieve/index.md) for full details. ## Mark a chat as read Mark every message in a chat as read, which sends a read receipt on iMessage/RCS. See the [Mark As Read API reference](/api/resources/chats/methods/mark_as_read/index.md). > **Note:** Calling mark-as-read on a [group chat](/guides/chats/group-chats/index.md) has no effect. Read receipts only exist in one-to-one iMessage and RCS conversations. ## Related - [Group Chats](/guides/chats/group-chats/index.md) — creating chats, managing participants, group name / icon, leaving a group - [Typing Indicators](/guides/chats/typing-indicators/index.md) — typing state inside a chat - [Chat Health](/guides/chats/chat-health/index.md) — how we score chat health and what it means for your line - [Sending Messages](/guides/messaging/sending-messages#message-parts/index.md) — posting messages to a chat - [API Reference: Chats](/api/resources/chats/index.md) --- # Sharing Contact Card URL: https://docs.linqapp.com/guides/chats/share-contact-card/ Sharing a contact card is the iMessage **Name and Photo Sharing** action: it pushes the contact card already configured on your sending number into a chat so the recipient’s device prompts them to save your name and photo. You must have a contact card set up first — see [Contact Cards](/guides/contact-cards/index.md) to create or update yours. **Before calling the share endpoint, make sure:** - A contact card exists for the `from` number — if none is configured the API returns [error `2012`](/error/codes/2xxx/2012/index.md). Create one via the [Contact Cards endpoints](/guides/contact-cards/index.md) or the [Linq dashboard](https://dashboard.linqapp.com/contact-cards). - Confirm the card activated (`is_active: true`) via the [Retrieve Contact Card](/api/resources/contact_card/methods/retrieve/index.md) endpoint before sharing. - The chat is an **iMessage** conversation — this is an iMessage-only affordance and has no effect on RCS or SMS chats. - There is at least one prior **outbound message** in the chat. ## Share the card Call the share endpoint on a specific chat — no request body needed. The card associated with the chat’s `from` phone number is sent automatically. See the [Share Contact Card API reference](/api/resources/chats/methods/share_contact_card/index.md). - [cURL](#tab-panel-28) - [TypeScript](#tab-panel-29) - [Python](#tab-panel-30) - [Go](#tab-panel-31) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/share_contact_card \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.chats.shareContactCard({chatId}); ``` ``` client.chats.share_contact_card({chat_id}) ``` ``` client.Chats.ShareContactCard(context.TODO(), {chatId}) ``` ## Sharing behavior Sharing is near-instantaneous and may not take effect in every chat due to limitations outside our control. There’s no confirmation the user saved it, so call the share endpoint **once per day after the first outbound** activity on that chat — this keeps giving them the option if they dismissed it. Calling the endpoint multiple times within 24h won’t present the option more than once. > **This is iMessage’s native identity-sharing feature, not a vCard (.vcf) attachment.** The recipient does not receive a file — iMessage surfaces your card as a system-level prompt to save your name and photo. If you want to send a contact’s details *as a file* (e.g. share someone else’s vCard), attach a `.vcf` via a media part instead — see [Sending Messages → Message parts](/guides/messaging/sending-messages#message-parts/index.md). ## Related - [Contact Cards](/guides/contact-cards/index.md) — set up, retrieve, and update the card that gets shared - [Chats](/guides/chats/index.md) — chat lifecycle operations - [Group Chats](/guides/chats/group-chats/index.md) — group-specific management - [API Reference: Share Contact Card](/api/resources/chats/methods/share_contact_card/index.md) --- # Typing Indicators URL: https://docs.linqapp.com/guides/chats/typing-indicators/ Typing indicators show recipients that someone is actively composing a message. They create a more natural, real-time feel in conversations. ## Sending typing indicators Start a typing indicator in a chat to show the recipient that you’re composing a message: - [cURL](#tab-panel-32) - [TypeScript](#tab-panel-33) - [Python](#tab-panel-34) - [Go](#tab-panel-35) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/typing \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.chats.typing.start({chatId}); ``` ``` client.chats.typing.start({chat_id}) ``` ``` client.Chats.Typing.Start(context.TODO(), {chatId}) ``` Stop the indicator when you’re done (it also clears automatically when you send a message): - [cURL](#tab-panel-36) - [TypeScript](#tab-panel-37) - [Python](#tab-panel-38) - [Go](#tab-panel-39) Terminal window ``` curl -X DELETE https://api.linqapp.com/api/partner/v3/chats/{chatId}/typing \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.chats.typing.stop({chatId}); ``` ``` client.chats.typing.stop({chat_id}) ``` ``` client.Chats.Typing.Stop(context.TODO(), {chatId}) ``` > **Tip:** Send a typing indicator before your application processes a response (e.g., while an AI agent generates a reply). This gives the recipient a natural “someone is typing” experience. The indicator automatically clears when you send the actual message. ## Receiving typing indicators Subscribe to typing indicator webhooks to know when a recipient is typing: | Event | Description | | ------------------------------- | ---------------------------- | | `chat.typing_indicator.started` | A participant started typing | | `chat.typing_indicator.stopped` | A participant stopped typing | See [Webhooks](/guides/webhooks/index.md) for setup instructions and the [Webhook Events API Reference](/api/resources/webhooks/index.md) for payload schemas. ## Important notes - **iMessage only** — Typing indicators are an iMessage feature and are not available on RCS or SMS. See [Protocol Selection](/guides/messaging/protocol-selection/index.md) for protocol capabilities. - **One-to-one chats only** — Typing indicators are **not supported in [group chats](/guides/chats/group-chats/index.md)**. They only work in one-to-one conversations. - **Auto-clear** — Typing indicators automatically clear when you send a message to the chat. - **Timeout** — Typing indicators expire after approximately 60 seconds if not refreshed or stopped. - **Best-effort delivery** — A `204` response means the request was accepted, not that the indicator was delivered. The chat must have had activity within the last 5 minutes for the indicator to reach the recipient. See the [Chats API Reference](/api/resources/chats/index.md) for the full typing indicator endpoint specification. --- # Contact Cards URL: https://docs.linqapp.com/guides/contact-cards/ Contact Card lets you set and share your contact information (name and profile photo) with chat participants via iMessage Name and Photo Sharing. Use `POST /v3/contact_card` to create or update a card for a phone number. Use `PATCH /v3/contact_card` to update an existing active card. Use `GET /v3/contact_card` to retrieve the active card(s) for your partner account. **Sharing behavior:** Sharing may not take effect in every chat due to limitations outside our control. We recommend calling the share endpoint once per day, after the first outbound activity. A contact card is **not** a vCard (.vcf) attachment — it’s iMessage’s native identity card, so recipients see “Acme Support” in place of “+1 (415) 555-1234”. Working with contact cards is a **two-step flow**: 1. **Configure once per phone number** — [create](#create-a-contact-card) (or [update](#update-a-contact-card)) the card on your sending number. This sets the identity but does **not** push it into any chat. 2. **Share into each chat** — [call the share endpoint](#share-a-contact-card) on a specific chat to actually prompt the recipient to save your name and photo. ## Create a contact card Create a contact card and apply it to one of your phone numbers. The card is stored in an inactive state first; once successfully applied it activates and `is_active` returns `true`. See the [Create Contact Card API reference](/api/resources/contact_card/methods/create/index.md) for the full endpoint specification. - [cURL](#tab-panel-44) - [TypeScript](#tab-panel-45) - [Python](#tab-panel-46) - [Go](#tab-panel-47) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/contact_card \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+15551234567", "first_name": "Acme", "last_name": "Support", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg" }' ``` ``` await client.contactCard.create({ phone_number: "+15551234567", first_name: "Acme", last_name: "Support", image_url: "https://cdn.linqapp.com/contact-card/example.jpg", }); ``` ``` client.contactCard.create( phone_number="+15551234567", first_name="Acme", last_name="Support", image_url="https://cdn.linqapp.com/contact-card/example.jpg", ) ``` ``` client.ContactCard.Create(context.TODO(), linq.ContactCardNewParams{ PhoneNumber: linq.F("+15551234567"), FirstName: linq.F("Acme"), LastName: linq.F("Support"), ImageUrl: linq.F("https://cdn.linqapp.com/contact-card/example.jpg"), }) ``` | Field | Required | Type | Description | | -------------- | -------- | -------- | ----------------------------------------------------- | | `phone_number` | Yes | `string` | E.164 phone number to associate the contact card with | | `first_name` | Yes | `string` | First name for the contact card. Required. | | `last_name` | No | `string` | Last name for the contact card. Optional. | | `image_url` | No | `string` | Profile image URL for the contact card. | Creating a card for a phone number that already has one returns [error `2014`](/error/codes/2xxx/2014/index.md). Use the update endpoint instead. **Creating a card does not share it.** Creating only configures the card on your phone number — recipients won’t see anything yet. To actually prompt them to save your name and photo, [call the share endpoint](#share-a-contact-card) on each chat after the first outbound message. Confirm the card activated (`is_active: true`) via the [Retrieve Contact Card](/api/resources/contact_card/methods/retrieve/index.md) endpoint before sharing. ## Retrieve contact cards Retrieve every card on your account, or filter to a single phone number with the `phone_number` query parameter. If no card exists for the queried number, the endpoint returns [error `2012`](/error/codes/2xxx/2012/index.md). See the [Retrieve Contact Card API reference](/api/resources/contact_card/methods/retrieve/index.md). ## Update a contact card Partially update the active card for a phone number — omitted fields retain their existing values. An active card must already exist, otherwise the request returns [error `2012`](/error/codes/2xxx/2012/index.md). See the [Update Contact Card API reference](/api/resources/contact_card/methods/update/index.md). ## Share a contact card Configuring a card is **not** the same as showing it to a recipient. Your card lives on your phone number until you push it into a chat with the share endpoint — a separate API call against `POST /v3/chats/{chatId}/share_contact_card`. See the [Share Contact Card API reference](/api/resources/chats/methods/share_contact_card/index.md). **Before calling share, make sure:** - A contact card exists for the chat’s `from` number, with `is_active: true`. If not, the API returns [error `2012`](/error/codes/2xxx/2012/index.md). - The chat is an **iMessage** conversation — sharing has no effect on RCS or SMS chats. - There is at least one prior **outbound message** in the chat. * [cURL](#tab-panel-40) * [TypeScript](#tab-panel-41) * [Python](#tab-panel-42) * [Go](#tab-panel-43) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/share_contact_card \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.chats.shareContactCard({chatId}); ``` ``` client.chats.share_contact_card({chat_id}) ``` ``` client.Chats.ShareContactCard(context.TODO(), {chatId}) ``` The endpoint takes no request body — the card associated with the chat’s `from` phone number is shared automatically. There’s no confirmation the recipient saved it, so it’s safe to call **once per day** after the first outbound message in that chat. See [Sharing Contact Card](/guides/chats/share-contact-card/index.md) for the full cadence and behavior notes. ## Related - [Sharing Contact Card](/guides/chats/share-contact-card/index.md) — push the configured card into an iMessage chat - [Error 2012 — contact card not found](/error/codes/2xxx/2012/index.md) - [Error 2014 — contact card already exists](/error/codes/2xxx/2014/index.md) - [API Reference: Contact Card](/api/resources/contact_card/index.md) --- # Vercel Chat SDK URL: https://docs.linqapp.com/guides/integrations/chat-sdk/ [Chat SDK](https://chat-sdk.dev) is Vercel’s framework for building chat agents. Write your bot’s logic once and run it across platforms through adapters. The Linq adapter is the iMessage and SMS channel — your agent sends and receives texts, media, and tapback reactions over Apple Messages and SMS, using the same handlers you’d write for any other platform. The adapter is built and maintained by Linq and lives at [github.com/linq-team/linq-chat-sdk](https://github.com/linq-team/linq-chat-sdk). ## Install Terminal window ``` npm install @linqapp/chat-sdk-adapter chat ``` `chat` is Vercel’s Chat SDK core; `@linqapp/chat-sdk-adapter` is the Linq channel. ## Quickstart ``` import { createLinqAdapter } from "@linqapp/chat-sdk-adapter"; import { Chat } from "chat"; const chat = new Chat({ userName: "mybot", adapters: { linq: createLinqAdapter({ apiKey: process.env.LINQ_API_KEY!, signingSecret: process.env.LINQ_WEBHOOK_SECRET!, }), }, }); chat.onDirectMessage(async (thread, message) => { await thread.subscribe(); await thread.post(`you said: ${message.text}`); }); chat.onReaction(["thumbs_up"], async (event) => { await event.thread.post("appreciate the tapback 🫡"); }); ``` Then route Linq webhooks to the adapter from any fetch-style handler: ``` // e.g. a Next.js / Nitro / Hono POST route export default async (request: Request) => chat.webhooks.linq(request); ``` ## Configuration | Option | Required | Description | | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `apiKey` | yes | Your Linq [API key](/getting-started/authentication/index.md). | | `signingSecret` | yes | The signing secret from your [webhook subscription](/guides/webhooks/subscriptions/index.md). Inbound requests are verified with HMAC-SHA256 over `{timestamp}.{body}` with replay protection. | | `baseURL` | no | Override the API base URL. | ## Receiving messages The adapter is webhook-driven. [Create a webhook subscription](/guides/webhooks/subscriptions/index.md) pointing at your route and subscribe to at least: - `message.received` - `reaction.added` - `reaction.removed` See [webhook events](/guides/webhooks/events/index.md) for the full payload reference. Any event type the adapter doesn’t handle is acknowledged with a `200` and ignored. ## What’s supported The adapter maps Linq onto the Chat SDK’s thread/message/reaction model, so standard APIs (`thread.post`, `thread.subscribe`, `onDirectMessage`, `onNewMessage`, `onReaction`) work unchanged. - **Text** — inbound and outbound, in DMs and [group chats](/guides/chats/group-chats/index.md). - **Media** — inbound images, audio, and files arrive as attachments; outbound `attachments`/`files` are sent as [media parts](/guides/messaging/attachments/index.md) (public HTTPS URLs by reference, or pre-uploaded for larger/raw files). - **Reactions** — iMessage tapbacks map to Chat SDK emoji both ways (see below). - **Typing indicators** — supported in DMs ([Linq rejects typing in groups](/guides/chats/typing-indicators/index.md)). - **Edits** — outbound message text can be edited. Streaming is buffered (recipients see one final message). Stickers, message deletion, and modals have no iMessage equivalent and are not supported. ## Reactions Standard tapbacks map to normalized Chat SDK emoji in both directions: | Linq tapback | Chat SDK emoji | | ------------ | -------------- | | `like` | `thumbs_up` | | `dislike` | `thumbs_down` | | `love` | `heart` | | `laugh` | `laugh` | | `emphasize` | `exclamation` | | `question` | `question` | Custom emoji reactions pass through the default resolver (e.g. `👍` → `thumbs_up`); anything unmapped falls back to the raw emoji. See [reactions](/guides/messaging/reactions/index.md) for how Linq models tapbacks. ## Thread IDs Thread IDs are stable and always take the form `linq:{chatId}`, so a conversation maps to the same Chat SDK thread whether it first arrives via webhook or API. ## Example app The repo includes a [full example](https://github.com/linq-team/linq-chat-sdk/tree/main/apps/api) — a server running one AI bot across Linq, Telegram, and WhatsApp from a single set of handlers. --- # Location Sharing URL: https://docs.linqapp.com/guides/location-sharing/ Request and retrieve real-time location data via iMessage. Use these endpoints to request a contact’s location, retrieve location data for contacts who are sharing with you, and subscribe to webhooks when someone starts or stops sharing their location. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. Location sharing is a **two-part flow**: 1. **Request** a participant’s location — they get a prompt asking them to share. ([Request a location](#request-a-location)) 2. **Read** the locations of everyone sharing with you in a chat, returned as [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946). ([Read shared locations](#read-shared-locations)) Subscribe to [webhooks](#webhooks) to be notified the moment someone starts or stops sharing, instead of polling. ## Request a location Send a location request to the other participant in a chat: `POST /v3/chats/{chatId}/location/request`. They receive a prompt asking them to share their current location. See the [Request Location API reference](/api/resources/chats/subresources/location/methods/request/index.md). - [cURL](#tab-panel-48) - [TypeScript](#tab-panel-49) - [Python](#tab-panel-50) - [Go](#tab-panel-51) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/location/request \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.chats.location.request({chatId}); ``` ``` client.chats.location.request({chat_id}) ``` ``` client.Chats.Location.Request(context.TODO(), {chatId}) ``` Requesting a location only works in **1:1 iMessage chats**. Requesting in a group chat, or in an SMS or RCS chat, returns HTTP `409` — the operation isn’t supported on that chat’s [service type](/guides/messaging/protocol-selection/index.md). See [Error Handling](/error/index.md) for response details. **Requesting is not the same as receiving.** A request only prompts the recipient — they choose whether to share, and for how long. Until they accept, [reading locations](#read-shared-locations) returns an empty `features` array. Subscribe to the [`location.sharing.started` webhook](#webhooks) to know when sharing actually begins. ## Read shared locations Retrieve the current location of everyone sharing with you in a chat: `GET /v3/chats/{chatId}/location`. See the [Get Location API reference](/api/resources/chats/subresources/location/methods/retrieve/index.md). - [cURL](#tab-panel-52) - [TypeScript](#tab-panel-53) - [Python](#tab-panel-54) - [Go](#tab-panel-55) Terminal window ``` curl https://api.linqapp.com/api/partner/v3/chats/{chatId}/location \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.chats.location.retrieve({chatId}); ``` ``` client.chats.location.retrieve({chat_id}) ``` ``` client.Chats.Location.Retrieve(context.TODO(), {chatId}) ``` The response is wrapped in the standard `{ "success": true, "data": ... }` envelope — the body is **not** a bare GeoJSON document. `data` is a GeoJSON `FeatureCollection` with one `Feature` per participant actively sharing. Works for both 1:1 and group chats — in a group, each sharing participant is a separate feature, identified by `properties.handle`. If no one is sharing yet, `data.features` is an empty array. Each feature’s `geometry.coordinates` are `[longitude, latitude]`, or `[longitude, latitude, altitude]` when altitude is available — note the **longitude-first** ordering per the GeoJSON spec. Feature `properties` include: | Field | Description | | ------------ | ------------------------------------------- | | `handle` | Phone number or email of the person sharing | | `address` | Full street address (when available) | | `locality` | City or locality name (when available) | | `updated_at` | When the location was last updated | ## Webhooks Two event types fire as sharing state changes. Subscribe via the [Webhook Subscriptions API](/api/resources/webhooks/index.md) and handle them like any other event — see [Webhook Events](/guides/webhooks/events/index.md) for the envelope and delivery guarantees. | Event | Fires when | | -------------------------- | ---------------------------------------------------- | | `location.sharing.started` | A participant starts sharing their location with you | | `location.sharing.stopped` | A participant stops sharing | Both payloads carry `shared_by` (their number) and `shared_with` (your number); `location.sharing.started` also includes `began_at` and, when sharing is time-boxed, `ends_at`. After a `started` event, call [Read shared locations](#read-shared-locations) to pull the current coordinates. ## Related - [Location Sharing API reference](/api/resources/chats/subresources/location/index.md) — request + retrieve endpoints - [Chats](/api/resources/chats/index.md) — location requests and reads are scoped to a chat - [Protocol Selection](/guides/messaging/protocol-selection/index.md) — why location requests require iMessage - [Webhooks](/guides/webhooks/index.md) — subscribe to `location.sharing.*` events - [Error Handling](/error/index.md) — `409` and other responses --- # Attachments URL: https://docs.linqapp.com/guides/messaging/attachments/ Send files (images, videos, documents, audio) with messages by providing a URL in a media part. Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios. ## Sending Media via URL (up to 10MB) Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part. ``` { "parts": [ { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" } ] } ``` This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.** ## Pre-Upload (required for files over 10MB) Use `POST /v3/attachments` when you want to: - **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB - **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time - **Reduce message send latency** — the file is already stored, so sending is faster **How it works:** 1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id` 2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content) 3. Reference the `attachment_id` in your media part when sending messages (no expiration) **Key difference:** When you provide an external `url`, we download and process the file on every send. When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely. ## Domain Allowlisting Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes: - `url` fields in media and voice memo message parts - `download_url` fields in attachment and upload response objects If your application enforces domain allowlists (e.g., for SSRF protection), add: ``` cdn.linqapp.com ``` ## Supported File Types - **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP - **Videos:** MP4, MOV, M4V - **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR - **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP - **Contact & Calendar:** VCF, ICS ## Audio: Attachment vs Voice Memo Audio files sent as media parts appear as **downloadable file attachments** in iMessage. To send audio as an **iMessage voice memo bubble** (with native inline playback UI), use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead. ## File Size Limits - **URL-based (`url` field):** 10MB maximum - **Pre-upload (`attachment_id`):** 100MB maximum ## Security & Ownership Every attachment is bound to the partner account that created or received it. The API enforces ownership on every operation that touches an attachment — sending, retrieving, deleting. **What this means for you:** - An attachment created under your API key can only be referenced by your API key. - Submitting another partner’s `attachment_id` returns `404 Not Found`. We do not disclose whether the id exists or belongs to someone else. - Submitting a CDN URL that resolves to another partner’s attachment is rejected before the send is attempted. - Ownership enforcement applies uniformly across send, create-chat, voice memo, retrieve, and delete operations. Every attachment-affecting endpoint requires a valid partner API key. Unauthenticated calls return `401 Unauthorized`. ## Attachment URL Patterns Attachment URLs in API responses and webhook payloads use one of two layouts, depending on the attachment’s tier: | Tier | URL pattern | TTL | | -------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Persistent (default) | `https://cdn.linqapp.com/attachments/partners/{partner_id}/{attachment_id}/{filename}` | Long-lived | | Ephemeral | Pre-signed URL pointing at the ephemeral prefix on `cdn.linqapp.com` | 15 minutes per signed URL — re-fetch via the API for a fresh URL | Inbound media you receive over webhooks uses the same layout your outbound sends produce, so the URL you store and the URL you build look identical — no special casing in your client. ## Ephemeral Attachments (Privacy Tier) For regulated or sensitive content, opt in to the **ephemeral attachments** tier by contacting your Linq support contact. You can request it at two scopes: | Scope | Effect | | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Partner-wide** | Every outbound and inbound attachment on every phone number under your account is routed through the ephemeral tier. | | **Per phone number** | Only the specified phone numbers route their attachments through the ephemeral tier. The rest stay on the persistent tier. | **Behavioral differences vs the persistent default:** | Aspect | Persistent | Ephemeral | | ----------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | | Download URL form | Long-lived CDN URL | Pre-signed URL with short TTL | | Retention floor | Indefinite (until you call `DELETE`) | **Hard backstop: 1 day** — even without an explicit `DELETE`, the platform removes the underlying bytes after 24 hours | | URL re-fetch | Not required | Fetch via `GET /v3/attachments/{attachmentId}` for a fresh signed URL after TTL expiry | | Cross-partner isolation | Enforced | Enforced | **When to choose ephemeral:** - Your downstream system processes the file immediately on receipt and does not need to re-read it later. - You have a compliance requirement that the platform must not retain attachments beyond a short window. - The content is high-sensitivity (PHI, financial documents, identity verification) and you do not want it sitting behind a long-lived URL. **Important:** ephemeral applies in *both directions* — outbound files you upload **and** inbound media received by the phone numbers in that scope. Download bytes you need to keep promptly, or fetch a fresh signed URL via the API when needed. ## Deleting an Attachment To permanently remove an attachment you own, use: ``` DELETE /v3/attachments/{attachmentId} Authorization: Bearer ``` **What this does:** 1. Verifies the attachment is owned by your account. Returns `404` otherwise. 2. Removes the underlying file from Linq storage. 3. Records an audit entry (timestamp, partner, attachment id). **Response codes:** | Status | Meaning | | --------------------------- | ---------------------------------------------------------------- | | `204 No Content` | Deletion succeeded. The attachment is removed from Linq storage. | | `400 Bad Request` | `attachmentId` is not a valid UUID. | | `401 Unauthorized` | Missing or invalid API key. | | `404 Not Found` | Attachment does not exist or is not owned by your account. | | `500 Internal Server Error` | Transient infrastructure issue — safe to retry. | **Effect on message history:** - Messages that referenced the deleted attachment remain visible. - The message part that pointed at the attachment is preserved with no attachment reference. - Webhook payloads previously delivered to you retain the original URL string, but downloads from that URL return `404` going forward. Deletion is **irreversible**. Once `204` is returned, the bytes are gone — there is no undelete. ## Inbound Media Flow When one of your phone numbers receives a message with media (image, video, audio, document), the platform: 1. Stores the file under your partner account. 2. Records metadata linked to the inbound message. 3. Delivers a webhook whose `parts[]` array includes a `media` part with a `url` pointing at `cdn.linqapp.com`. 4. If the receiving phone is opted in to ephemeral, the `url` is a short-TTL signed URL. You can acknowledge the webhook without fetching the file inline, and lazy-load via `GET /v3/attachments/{attachmentId}` later. For ephemeral attachments, retrieving via the API always returns a freshly-signed URL. ## Data Lifecycle Summary | Data | Persistent tier | Ephemeral tier | | --------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). ## Compliance Checklist If you’re integrating Linq under a security or privacy review, here is the short list: - Allowlist exactly one outbound domain: `cdn.linqapp.com`. - Decide whether you need ephemeral attachments (high-sensitivity content) — request enablement through your Linq support contact. - Implement `DELETE /v3/attachments/{attachmentId}` calls in your deletion workflow. - Persist any attachments your application needs long-term — Linq is the authoritative source until you delete, but the ephemeral tier auto-purges after 1 day. - For audit: every deletion is logged on Linq’s side. Surface a confirmation in your application UI based on the `204` response. - For end-user “right to delete” requests: enumerate attachment ids and `DELETE` each. The platform does not provide a partner-wide wipe endpoint — deletion is per-attachment by design. ## SDK examples The overview above shows JSON payloads. The same operations from the TypeScript and Python SDKs: ### Send media by URL - [cURL](#tab-panel-60) - [TypeScript](#tab-panel-61) - [Python](#tab-panel-62) - [Go](#tab-panel-63) 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": "Check out this photo!" }, { "type": "media", "url": "https://example.com/photo.jpg" } ] } }' ``` ``` await client.chats.messages.send({chatId}, { message: { parts: [ { type: "text", value: "Check out this photo!", }, { type: "media", url: "https://example.com/photo.jpg", }, ], }, }); ``` ``` client.chats.messages.send( {chat_id}, message={ "parts": [ { "type": "text", "value": "Check out this photo!", }, { "type": "media", "url": "https://example.com/photo.jpg", }, ], }, ) ``` ``` 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("Check out this photo!"), }, map[string]any{ Type: linq.F("media"), Url: linq.F("https://example.com/photo.jpg"), }, }), }), }) ``` ### Request a pre-signed upload URL - [cURL](#tab-panel-56) - [TypeScript](#tab-panel-57) - [Python](#tab-panel-58) - [Go](#tab-panel-59) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/attachments \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "photo.jpg", "content_type": "image/jpeg", "size_bytes": 1024000 }' ``` ``` await client.attachments.create({ filename: "photo.jpg", content_type: "image/jpeg", size_bytes: 1024000, }); ``` ``` client.attachments.create( filename="photo.jpg", content_type="image/jpeg", size_bytes=1024000, ) ``` ``` client.Attachments.Create(context.TODO(), linq.AttachmentNewParams{ Filename: linq.F("photo.jpg"), ContentType: linq.F("image/jpeg"), SizeBytes: linq.F(1024000), }) ``` ### Send a pre-uploaded attachment - [cURL](#tab-panel-64) - [TypeScript](#tab-panel-65) - [Python](#tab-panel-66) - [Go](#tab-panel-67) 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": "media", "attachment_id": "550e8400-e29b-41d4-a716-446655440000" } ] } }' ``` ``` await client.chats.messages.send({chatId}, { message: { parts: [ { type: "media", attachment_id: "550e8400-e29b-41d4-a716-446655440000", }, ], }, }); ``` ``` client.chats.messages.send( {chat_id}, message={ "parts": [ { "type": "media", "attachment_id": "550e8400-e29b-41d4-a716-446655440000", }, ], }, ) ``` ``` 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("media"), AttachmentId: linq.F("550e8400-e29b-41d4-a716-446655440000"), }, }), }), }) ``` ## Related - [Sending Messages](/guides/messaging/sending-messages/index.md) — basics of posting a message to a chat - [Voice Memos](/guides/messaging/voice-memos/index.md) — audio as an inline iMessage voice bubble - [API Reference: Attachments](/api/resources/attachments/index.md) --- # iMessage Apps URL: https://docs.linqapp.com/guides/messaging/imessage-apps/ An **iMessage app** is a Messages app extension — a mini-app that runs inside Messages. With the `imessage_app` message part you send a tappable card that opens your app at a URL you provide. Use it to hand a conversation off into an interactive experience — a game move, a checkout, an RSVP — that lives inside Messages. iMessage apps are sent as a message **part** with `type: "imessage_app"`, in place of the `text`, `media`, and `link` parts you already use. ## When to use an app card App cards are for **branded, interactive experiences backed by your own iMessage app**. A card can carry a [preview image](#image_url--a-preview-image-everyone-sees), but for a plain image or link with no app behind it, the simpler parts are the right tool: | You want… | Use | | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | A plain image or link, with no app behind it | a [rich link](/guides/messaging/rich-link-previews/index.md) or [media attachment](/guides/messaging/attachments/index.md) | | A branded, interactive card — optionally with a preview image | an **iMessage app** (this guide) | > **Prerequisite: your own iMessage app.** A card renders the Messages extension named by `team_id` + `bundle_id`, drawing its interactive content from your `url` — so recipients only get the live experience when that extension is a real, shipping app they have installed. What you supply directly through the API is the static card: the captions and an optional preview image. ## Constraints iMessage app parts have stricter rules than other parts: - **iMessage only.** They never fall back to SMS or RCS. If you explicitly request SMS or RCS alongside an app part, the send is rejected with [`2018` (IMessageAppServiceUnsupported)](/error/codes/2xxx/2018/index.md). If the recipient simply isn’t reachable over iMessage, the send is accepted and then fails asynchronously with a `message.failed` webhook carrying [`4005` (RecipientUnsupportedMessageType)](/error/codes/4xxx/4005/index.md). [Check capability](/guides/messaging/protocol-selection#protocol-capabilities/index.md) before sending. - **Must be the only part.** An `imessage_app` part cannot be combined with `text`, `media`, or `link` parts in the same message. ## Sending an iMessage app Send one as the first message in a new chat with [Create Chat](/api/resources/chats/methods/create/index.md): - [cURL](#tab-panel-69) - [TypeScript](#tab-panel-70) - [Python](#tab-panel-71) - [Go](#tab-panel-72) 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": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension" }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "layout": { "caption": "Example App", "subcaption": "You said: hello" } } ] } }' ``` ``` await client.chats.create({ from: "+12052535597", to: ["+12052532136"], message: { parts: [ { type: "imessage_app", app: { name: "Example App", team_id: "A1B2C3D4E5", bundle_id: "com.example.app.MessageExtension", }, url: "https://app.example.com/card?id=abc123", fallback_text: "Open in Example App", layout: { caption: "Example App", subcaption: "You said: hello", }, }, ], }, }); ``` ``` client.chats.create( from_="+12052535597", to=["+12052532136"], message={ "parts": [ { "type": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension", }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "layout": { "caption": "Example App", "subcaption": "You said: hello", }, }, ], }, ) ``` ``` 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("imessage_app"), App: linq.F(map[string]any{ Name: linq.F("Example App"), TeamId: linq.F("A1B2C3D4E5"), BundleId: linq.F("com.example.app.MessageExtension"), }), Url: linq.F("https://app.example.com/card?id=abc123"), FallbackText: linq.F("Open in Example App"), Layout: linq.F(map[string]any{ Caption: linq.F("Example App"), Subcaption: linq.F("You said: hello"), }), }, }), }), }) ``` To send into an existing chat, post the same part to [Send Message](/api/resources/chats/subresources/messages/methods/send/index.md): - [cURL](#tab-panel-73) - [TypeScript](#tab-panel-74) - [Python](#tab-panel-75) - [Go](#tab-panel-76) 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": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension" }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "layout": { "caption": "Example App", "subcaption": "You said: hello" } } ] } }' ``` ``` await client.chats.messages.send({chatId}, { message: { parts: [ { type: "imessage_app", app: { name: "Example App", team_id: "A1B2C3D4E5", bundle_id: "com.example.app.MessageExtension", }, url: "https://app.example.com/card?id=abc123", fallback_text: "Open in Example App", layout: { caption: "Example App", subcaption: "You said: hello", }, }, ], }, }); ``` ``` client.chats.messages.send( {chat_id}, message={ "parts": [ { "type": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension", }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "layout": { "caption": "Example App", "subcaption": "You said: hello", }, }, ], }, ) ``` ``` 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("imessage_app"), App: linq.F(map[string]any{ Name: linq.F("Example App"), TeamId: linq.F("A1B2C3D4E5"), BundleId: linq.F("com.example.app.MessageExtension"), }), Url: linq.F("https://app.example.com/card?id=abc123"), FallbackText: linq.F("Open in Example App"), Layout: linq.F(map[string]any{ Caption: linq.F("Example App"), Subcaption: linq.F("You said: hello"), }), }, }), }), }) ``` To show a preview image on the card, set `layout.image_url` — optionally with `image_title` and `image_subtitle` overlaid on it: - [cURL](#tab-panel-77) - [TypeScript](#tab-panel-78) - [Python](#tab-panel-79) - [Go](#tab-panel-80) 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": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension" }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "layout": { "caption": "Example App", "subcaption": "You said: hello", "image_url": "https://cdn.linqapp.com/example/card-preview.jpg", "image_title": "Table for 2", "image_subtitle": "Tonight, 7:30 PM" } } ] } }' ``` ``` await client.chats.messages.send({chatId}, { message: { parts: [ { type: "imessage_app", app: { name: "Example App", team_id: "A1B2C3D4E5", bundle_id: "com.example.app.MessageExtension", }, url: "https://app.example.com/card?id=abc123", fallback_text: "Open in Example App", layout: { caption: "Example App", subcaption: "You said: hello", image_url: "https://cdn.linqapp.com/example/card-preview.jpg", image_title: "Table for 2", image_subtitle: "Tonight, 7:30 PM", }, }, ], }, }); ``` ``` client.chats.messages.send( {chat_id}, message={ "parts": [ { "type": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension", }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "layout": { "caption": "Example App", "subcaption": "You said: hello", "image_url": "https://cdn.linqapp.com/example/card-preview.jpg", "image_title": "Table for 2", "image_subtitle": "Tonight, 7:30 PM", }, }, ], }, ) ``` ``` 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("imessage_app"), App: linq.F(map[string]any{ Name: linq.F("Example App"), TeamId: linq.F("A1B2C3D4E5"), BundleId: linq.F("com.example.app.MessageExtension"), }), Url: linq.F("https://app.example.com/card?id=abc123"), FallbackText: linq.F("Open in Example App"), Layout: linq.F(map[string]any{ Caption: linq.F("Example App"), Subcaption: linq.F("You said: hello"), ImageUrl: linq.F("https://cdn.linqapp.com/example/card-preview.jpg"), ImageTitle: linq.F("Table for 2"), ImageSubtitle: linq.F("Tonight, 7:30 PM"), }), }, }), }), }) ``` To always show the static `layout` card — even to recipients who have your app — set `interactive: false`: - [cURL](#tab-panel-81) - [TypeScript](#tab-panel-82) - [Python](#tab-panel-83) - [Go](#tab-panel-84) 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": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension" }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "interactive": false, "layout": { "caption": "Example App", "subcaption": "You said: hello" } } ] } }' ``` ``` await client.chats.messages.send({chatId}, { message: { parts: [ { type: "imessage_app", app: { name: "Example App", team_id: "A1B2C3D4E5", bundle_id: "com.example.app.MessageExtension", }, url: "https://app.example.com/card?id=abc123", fallback_text: "Open in Example App", interactive: false, layout: { caption: "Example App", subcaption: "You said: hello", }, }, ], }, }); ``` ``` client.chats.messages.send( {chat_id}, message={ "parts": [ { "type": "imessage_app", "app": { "name": "Example App", "team_id": "A1B2C3D4E5", "bundle_id": "com.example.app.MessageExtension", }, "url": "https://app.example.com/card?id=abc123", "fallback_text": "Open in Example App", "interactive": False, "layout": { "caption": "Example App", "subcaption": "You said: hello", }, }, ], }, ) ``` ``` 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("imessage_app"), App: linq.F(map[string]any{ Name: linq.F("Example App"), TeamId: linq.F("A1B2C3D4E5"), BundleId: linq.F("com.example.app.MessageExtension"), }), Url: linq.F("https://app.example.com/card?id=abc123"), FallbackText: linq.F("Open in Example App"), Interactive: linq.F(false), Layout: linq.F(map[string]any{ Caption: linq.F("Example App"), Subcaption: linq.F("You said: hello"), }), }, }), }), }) ``` ## The `imessage_app` part ### `app` — which extension backs the card | Field | Required | Description | | -------------- | -------- | -------------------------------------------------------------------------------------------------------- | | `name` | yes | Display name, shown by Messages’ fallback UI (1–64 chars). | | `team_id` | yes | The app’s 10-character uppercase team identifier. | | `bundle_id` | yes | Bundle identifier of the Messages app extension. | | `app_store_id` | no | App Store id (integer). When set, recipients without the app installed see a **Get the app** affordance. | The identity is the **rendering key, not just a label**: the card *becomes* the app you name and is drawn by that app’s extension, so you normally pass your own app’s identity. > **An unrecognized identity silently renders as plain text.** If `team_id` + `bundle_id` don’t match a Messages extension the recipient has installed, the card falls back to caption text with **no error** — the usual cause of “my card shows text only.” Verify the identity against your shipping app. ### `url` and `fallback_text` - **`url`** — an HTTPS URL the backing app’s extension reads to render the card. It’s opaque to Messages; the extension interprets it and draws the rich content from it (for example, a reservation app might resolve a specific listing from a query parameter and render its photo and details). Change the `url` to change what the card shows. Max 2048 characters. - **`fallback_text`** — text shown where the card can’t render (notifications, lock screen). Defaults to the caption when omitted. - **`interactive`** *(default `true`)* — whether the card renders as your app’s live, interactive experience for recipients who have your app installed. Leave it `true` for the rich in-Messages experience; set it to `false` to always show the static `layout` card instead — even to recipients who have your app. Recipients without your app always see the static card regardless of this flag. See [How the card renders](#how-the-card-renders). ### `layout` — what the recipient sees The message renders as a card. At least one layout field — any of the four captions, or `image_url` — must be set, or the card renders as an empty bubble. | Field | Position | | --------------------- | ------------------------------------------ | | `caption` | top-left, bold (primary label) | | `subcaption` | left, below `caption` | | `trailing_caption` | top-right | | `trailing_subcaption` | right, below `trailing_caption` | | `image_url` | preview image at the top of the card | | `image_title` | bold text overlaid on the image | | `image_subtitle` | overlaid on the image, below `image_title` | > The small icon shown beside the caption is not a layout field: it’s always the app’s own icon (the installed app’s, or the App Store icon from `app_store_id`), and can’t be set per message. #### `image_url` — a preview image everyone sees Set `layout.image_url` to an HTTPS URL of an image (max 2048 chars) and it renders as a preview image at the top of the card — for **all** recipients, whether or not they have your app installed. The URL is fetched at send time; an unreachable URL, or one that doesn’t resolve to an image, is rejected with a validation error. Two optional text fields render overlaid on the image (max 512 chars each): - **`image_title`** — bold, overlaid on the image. - **`image_subtitle`** — beneath `image_title`. Both require `image_url` — setting either without it is rejected, since there’s no image to overlay. > **The image needs an established chat.** The preview image renders in chats with inbound activity — the recipient has sent you at least one message. In a brand-new, outbound-only chat the card still delivers, but with captions only. Updating a card with a new `image_url` [replaces the image in place](#updating-a-card-in-place). ## How the card renders What a recipient sees depends on whether they have the backing app installed **and** on the `interactive` flag: - **Has the app, `interactive: true` (default)** → the app’s Messages extension renders a **rich, interactive card from your `url`** — photo, details, and any interactive UI. The platform doesn’t draw this; the extension does, keyed off the app identity (`team_id` + `bundle_id`) and the `url`. If the identity doesn’t match an extension the recipient has, the card falls back to text. - **Has the app, `interactive: false`** → they see the **static `layout` card** instead of the live experience — the same card a recipient without the app would see. - **Doesn’t have the app** → they see your static `layout` card — the captions, plus the preview image when you set `image_url` — and a **Get the app** affordance when you set `app_store_id`. The `interactive` flag makes no difference here. This is why a card “renders the app inside the bubble” by default: that *is* the extension drawing your `url`. Set `interactive: false` when you’d rather everyone see the same static caption card — for example a status update that shouldn’t open an interactive surface. The card content you supply directly is the `layout` — captions and preview image — in either mode. > **The image can come from you; the icon always comes from the app.** The static card’s image is your `layout.image_url`. Without one, an image only appears for recipients whose installed extension draws its own from your `url`. The small icon beside the caption is always the app’s icon (the installed app’s, or the App Store icon from `app_store_id`) — it isn’t something you set per message. ## Receiving iMessage apps Inbound messages that contain an iMessage app carry an `imessage_app` part in the [`message.received`](/guides/webhooks/events/index.md) webhook payload, in place of the usual `text`, `media`, and `link` parts. The part mirrors the structure above, so your handler can read the app identity, `url`, and `layout` of a card a recipient sends you. ## Updating a card in place A delivered iMessage app card can be replaced in place — useful for live sessions like a game move redrawing the board, or an order status that changes after delivery. Send the new content to [Update App Card](/api/resources/messages/methods/update_app_card/index.md), referencing the original message: - [cURL](#tab-panel-68) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/messages/{messageId}/update \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/card?game=7f3a&move=2", "fallback_text": "Score update", "layout": { "caption": "Score: 2 – 1" } }' ``` The update inherits the original card’s app identity and replaces the delivered card rather than posting a second bubble. A few rules: - The referenced message must be an `imessage_app` card **you sent** — inbound cards can’t be updated (`400`). - The card must already be **delivered** — if you get a `409`, retry after its `message.delivered` webhook. - Only `url`, `fallback_text`, `interactive`, and `layout` change. The app identity (`team_id`, `bundle_id`, name) is fixed for the life of the card. - **A new `layout.image_url` swaps the card’s preview image in place** — the delivered card redraws with the new image (and any new `image_title`/`image_subtitle`). - **You can switch a card between interactive and static in place** by setting `interactive` on the update — send `interactive: false` to convert a live card to a static one, or `interactive: true` to convert it back. `interactive` defaults to `true` when omitted and is **not** inherited from the original card, so re-send `interactive: false` on each update to keep a static card static. - The update is delivered as a **new message** with its own id and its own `message.sent` / `message.delivered` / `message.failed` lifecycle. To update again, reference the **new** message id. --- # Messaging URL: https://docs.linqapp.com/guides/messaging/ Messages are individual communications within a chat thread. Messages can include text, media attachments, rich link previews, special effects (like confetti or fireworks), and reactions. All messages are associated with a specific chat and sent from a phone number you own. Messages support delivery status tracking, read receipts, and editing capabilities. ## Rich Link Previews Send a URL as a `link` part to deliver it with a rich preview card showing the page’s title, description, and image (when available). A `link` part must be the **only** part in the message — it cannot be combined with text or media parts. To send a URL without a preview card, include it in a `text` part instead. **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. ## Ephemeral Messages (Privacy Tier) For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. You can request it at two scopes: | Scope | Effect | | -------------------- | ------------------------------------------------------------------------------------------------------------------------- | | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | **Behavioral differences vs the standard default:** | Aspect | Standard | Ephemeral | | ----------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | | Cross-partner isolation | Enforced | Enforced | **How the 24-hour window works:** - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. **What you observe:** - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. **When to choose ephemeral:** - You have a compliance requirement that the platform must not retain message content beyond a short window. - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. ## Related - [Sending Messages](/guides/messaging/sending-messages/index.md) — start, reply, idempotency - [Message Details](/guides/messaging/message-details/index.md) — retrieve, update, delete - [Attachments](/guides/messaging/attachments/index.md) — media, files, pre-upload - [Voice Memos](/guides/messaging/voice-memos/index.md) - [Rich Link Previews](/guides/messaging/rich-link-previews/index.md) - [Reactions](/guides/messaging/reactions/index.md) - [Message Effects](/guides/messaging/message-effects/index.md) - [Protocol Selection](/guides/messaging/protocol-selection/index.md) — iMessage vs SMS vs RCS --- # Message Details URL: https://docs.linqapp.com/guides/messaging/message-details/ Once a message has been sent, you can fetch its current state by ID or walk an entire reply thread. For sending, editing, deleting, and replying, see [Sending Messages](/guides/messaging/sending-messages/index.md). ## Get a single message Fetch one message by ID, including its parts, reactions, delivery metadata, and thread pointer. See the [Retrieve Message API reference](/api/resources/messages/methods/retrieve/index.md). ## Get thread messages Given any message ID in a thread, retrieve the originator and all replies. Threads are built by passing [`reply_to`](/guides/messaging/sending-messages#replying-to-messages/index.md) when sending. If the supplied message isn’t part of a thread, a single-message result is returned. See the [List Messages Thread API reference](/api/resources/messages/methods/list_messages_thread/index.md). **Query parameters:** - `cursor` — pagination cursor from the previous response’s `next_cursor`. Omit for the first page. - `limit` — page size (max 100). - `order` — `asc` (oldest first, default) or `desc`. ## Related - [Sending Messages](/guides/messaging/sending-messages/index.md) — post, edit, delete, and reply - [Reactions](/guides/messaging/reactions/index.md) — the reactions attached to each message part - [API Reference: Messages](/api/resources/messages/index.md) --- # Message Effects URL: https://docs.linqapp.com/guides/messaging/message-effects/ Message effects are iMessage-specific visual animations that play when a recipient opens your message. They add personality and emphasis to conversations. For the basics of sending messages, see [Sending Messages](/guides/messaging/sending-messages/index.md). The `effect` field is an object with a `type` (`screen` or `bubble`) and a `name`: ``` "effect": { "type": "screen", "name": "confetti" } ``` ## Screen effects Screen effects fill the entire screen with an animation. Use `"type": "screen"`: | Name | Description | | ---------------- | -------------------------------------------------- | | `confetti` | Colorful confetti falls from the top of the screen | | `fireworks` | Fireworks explode across the screen | | `lasers` | Laser beams sweep across the screen | | `sparkles` | Sparkles glitter across the screen | | `celebration` | Streamers and confetti burst out | | `hearts` | Hearts float up from the message | | `love` | A large heart floats up from the message | | `balloons` | Balloons float up from the bottom | | `happy_birthday` | A “Happy Birthday” banner with confetti | | `echo` | The message echoes and repeats across the screen | | `spotlight` | A spotlight illuminates the message | ## Bubble effects Bubble effects animate the message bubble itself. Use `"type": "bubble"`: | Name | Description | | ----------- | -------------------------------------------------------------- | | `slam` | The message slams down onto the screen | | `loud` | The message grows large and shakes | | `gentle` | The message appears small and delicate | | `invisible` | The message is blurred until the recipient swipes to reveal it | ## Sending a message with an effect Add the `effect` object inside `message`, alongside `parts`: - [cURL](#tab-panel-85) - [TypeScript](#tab-panel-86) - [Python](#tab-panel-87) - [Go](#tab-panel-88) 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": "Congratulations! 🎉" } ], "effect": { "type": "screen", "name": "confetti" } } }' ``` ``` await client.chats.messages.send({chatId}, { message: { parts: [ { type: "text", value: "Congratulations! 🎉", }, ], effect: { type: "screen", name: "confetti", }, }, }); ``` ``` client.chats.messages.send( {chat_id}, message={ "parts": [ { "type": "text", "value": "Congratulations! 🎉", }, ], "effect": { "type": "screen", "name": "confetti", }, }, ) ``` ``` 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("Congratulations! 🎉"), }, }), Effect: linq.F(map[string]any{ Type: linq.F("screen"), Name: linq.F("confetti"), }), }), }) ``` ## Important notes - **iMessage only** — Effects are an iMessage feature. They are silently ignored when sending via RCS or SMS. See [Protocol Selection](/guides/messaging/protocol-selection/index.md) for protocol capabilities. - **One effect per message** — You can only apply a single effect to each message. - **Playback** — Effects play once when the recipient first opens the message. They can be replayed by the recipient. - **Combine with media** — Effects work with both text and media message parts. --- # Protocol Selection URL: https://docs.linqapp.com/guides/messaging/protocol-selection/ The Linq API supports sending messages via iMessage, RCS, and SMS. You can let the API automatically select the best protocol or explicitly choose one. See the [Create Chat](/api/resources/chats/methods/create/index.md) API reference for the full specification. ## Automatic selection (default) When `preferred_service` is omitted, the API uses the full fallback chain: **iMessage → RCS → SMS**. ## Explicit protocol selection Use `preferred_service` inside the `message` object to control which protocol is used: | Value | Behavior | | ---------- | ------------------------------------------------------------------------------------ | | `iMessage` | iMessage only. No fallback — send fails if the recipient is unavailable on iMessage. | | `RCS` | RCS if supported, otherwise SMS. Never uses iMessage. | | `SMS` | RCS if supported, otherwise SMS. Never uses iMessage. | > **`preferred_service` vs `service`:** `preferred_service` is what you requested. The `service` field on the response is what was actually used for delivery. - [cURL](#tab-panel-89) - [TypeScript](#tab-panel-90) - [Python](#tab-panel-91) - [Go](#tab-panel-92) 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": { "preferred_service": "iMessage", "parts": [ { "type": "text", "value": "Sent via iMessage only" } ] } }' ``` ``` await client.chats.create({ from: "+12052535597", to: ["+12052532136"], message: { preferred_service: "iMessage", parts: [ { type: "text", value: "Sent via iMessage only", }, ], }, }); ``` ``` client.chats.create( from_="+12052535597", to=["+12052532136"], message={ "preferred_service": "iMessage", "parts": [ { "type": "text", "value": "Sent via iMessage only", }, ], }, ) ``` ``` client.Chats.Create(context.TODO(), linq.ChatNewParams{ From: linq.F("+12052535597"), To: linq.F([]string{"+12052532136"}), Message: linq.F(map[string]any{ PreferredService: linq.F("iMessage"), Parts: linq.F([]any{ map[string]any{ Type: linq.F("text"), Value: linq.F("Sent via iMessage only"), }, }), }), }) ``` ## Protocol capabilities Not all features are available on every protocol: | Feature | iMessage | RCS | SMS | | ------------------------------------------------------------------ | -------- | ------- | --- | | Text messages | Yes | Yes | Yes | | Images & video | Yes | Yes | MMS | | Read receipts | Yes | Yes | No | | Delivery receipts | Yes | Yes | No | | [Typing indicators](/guides/chats/typing-indicators/index.md) | Yes | No | No | | [Reactions](/guides/messaging/reactions/index.md) / tapbacks | Yes | Yes | No | | [Message effects](/guides/messaging/message-effects/index.md) | Yes | No | No | | [Group chats](/guides/chats/group-chats/index.md) | Yes | Yes | MMS | | Message threading | Yes | No | No | | Rich link previews | Yes | Yes | No | | [Voice memos](/guides/messaging/voice-memos/index.md) | Yes | Yes | No | | [File attachments](/guides/messaging/attachments/index.md) (100MB) | Yes | Limited | No | | Text decorations | Yes | No | No | ## When to choose a protocol - **iMessage** — When you need iMessage-only features like message effects or text decorations. Be aware: delivery fails if the recipient isn’t on iMessage. - **RCS** — When you want rich messaging on Android (reactions, read receipts, threading) but still want SMS as a fallback. - **SMS** — Equivalent to RCS in behavior: uses RCS if supported, otherwise SMS. Never iMessage. - **Omit** — Best for maximum reach. The API picks the richest available protocol automatically. > **Note:** Use the capability check endpoints to verify recipient support before specifying `iMessage`. See [Capability Checks](/guides/chats/capability-checks/index.md) for details. --- # Reactions URL: https://docs.linqapp.com/guides/messaging/reactions/ React to any message with built-in iMessage tapbacks or custom Unicode emoji. Reactions are an iMessage feature — see [Protocol Selection](/guides/messaging/protocol-selection/index.md) for protocol capabilities. See the [Reactions API Reference](/api/resources/messages/methods/add_reaction/index.md) for the full endpoint specification. ## Adding a reaction - [cURL](#tab-panel-93) - [TypeScript](#tab-panel-94) - [Python](#tab-panel-95) - [Go](#tab-panel-96) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/messages/{messageId}/reactions \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operation": "add", "type": "love" }' ``` ``` await client.messages.addReaction({messageId}, { operation: "add", type: "love", }); ``` ``` client.messages.add_reaction( {message_id}, operation="add", type="love", ) ``` ``` client.Messages.AddReaction(context.TODO(), {messageId}, linq.MessageAddReactionParams{ Operation: linq.F("add"), Type: linq.F("love"), }) ``` ## Built-in reaction types These map to the standard iMessage tapback reactions: | Type | Description | | ----------- | ----------------- | | `love` | Heart | | `like` | Thumbs up | | `dislike` | Thumbs down | | `laugh` | Ha ha | | `emphasize` | Exclamation marks | | `question` | Question mark | ## Custom emoji reactions Send any Unicode emoji as a reaction: - [cURL](#tab-panel-97) - [TypeScript](#tab-panel-98) - [Python](#tab-panel-99) - [Go](#tab-panel-100) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/messages/{messageId}/reactions \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operation": "add", "type": "custom", "custom_emoji": "😍" }' ``` ``` await client.messages.addReaction({messageId}, { operation: "add", type: "custom", custom_emoji: "😍", }); ``` ``` client.messages.add_reaction( {message_id}, operation="add", type="custom", custom_emoji="😍", ) ``` ``` client.Messages.AddReaction(context.TODO(), {messageId}, linq.MessageAddReactionParams{ Operation: linq.F("add"), Type: linq.F("custom"), CustomEmoji: linq.F("😍"), }) ``` ## Targeting multipart messages For messages with multiple parts, target a specific part using `part_index` (0-based): - [cURL](#tab-panel-101) - [TypeScript](#tab-panel-102) - [Python](#tab-panel-103) - [Go](#tab-panel-104) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/messages/{messageId}/reactions \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operation": "add", "type": "laugh", "part_index": 1 }' ``` ``` await client.messages.addReaction({messageId}, { operation: "add", type: "laugh", part_index: 1, }); ``` ``` client.messages.add_reaction( {message_id}, operation="add", type="laugh", part_index=1, ) ``` ``` client.Messages.AddReaction(context.TODO(), {messageId}, linq.MessageAddReactionParams{ Operation: linq.F("add"), Type: linq.F("laugh"), PartIndex: linq.F(1), }) ``` If `part_index` is omitted, the reaction applies to the first part. ## Removing reactions Use the same endpoint with `"operation": "remove"` and the matching `type` you originally added: - [cURL](#tab-panel-105) - [TypeScript](#tab-panel-106) - [Python](#tab-panel-107) - [Go](#tab-panel-108) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/messages/{messageId}/reactions \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operation": "remove", "type": "like" }' ``` ``` await client.messages.addReaction({messageId}, { operation: "remove", type: "like", }); ``` ``` client.messages.add_reaction( {message_id}, operation="remove", type="like", ) ``` ``` client.Messages.AddReaction(context.TODO(), {messageId}, linq.MessageAddReactionParams{ Operation: linq.F("remove"), Type: linq.F("like"), }) ``` ## Sticker attachments A contact can attach a sticker to any message part in iMessage. This is distinct from a custom sticker reaction — the `type` is `"sticker"` and the payload includes image metadata. Sticker attachments are **inbound only**; the API does not support sending them. They appear inside `parts[].reactions[]` on the message and trigger a [`reaction.added`](/guides/webhooks/events/#reaction-events/index.md) event: ``` { "parts": [ { "type": "text", "value": "Hey yea", "reactions": [ { "type": "sticker", "is_me": false, "custom_emoji": null, "sticker": { "file_name": "sticker-abc123.png", "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/550e8400/sticker-abc123.png", "width": 320, "height": 320 }, "handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null } } ] } ] } ``` ## Reaction webhook events | Event | Description | | ------------------ | ------------------------------------- | | `reaction.added` | A reaction was added to a message | | `reaction.removed` | A reaction was removed from a message | Webhook payloads include `reaction_type`, `message_id`, `part_index`, and sender information. See [Reaction events](/guides/webhooks/events#reaction-events/index.md) for full payload details. --- # Rich Link Previews URL: https://docs.linqapp.com/guides/messaging/rich-link-previews/ A **link part** renders a URL as a rich inline card — title, description, and preview image — instead of a bare blue-link string. It’s the same affordance you see when you paste a URL into Messages manually. Rich link previews are available on iMessage and RCS. On SMS, link parts fall back to a plain text URL. ## Sending a link part - [cURL](#tab-panel-109) - [TypeScript](#tab-panel-110) - [Python](#tab-panel-111) - [Go](#tab-panel-112) 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": "link", "value": "https://linqapp.com" } ] } }' ``` ``` await client.chats.messages.send({chatId}, { message: { parts: [ { type: "link", value: "https://linqapp.com", }, ], }, }); ``` ``` client.chats.messages.send( {chat_id}, message={ "parts": [ { "type": "link", "value": "https://linqapp.com", }, ], }, ) ``` ``` 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("link"), Value: linq.F("https://linqapp.com"), }, }), }), }) ``` ## Constraints | Rule | Value | | ---------------------------------------- | ---------------------------------------------------- | | Must be the **only** part in the message | A link part cannot be mixed with text or media parts | | URL max length | 2,048 characters | | URL protocol | HTTPS required for preview generation | | Fallback on SMS | Bare text URL | Violating the “only part” rule returns [error `1004`](/error/codes/1xxx/1004/index.md) (*Invalid message content*). ## How previews are generated The preview (title, description, hero image) is fetched from the target page’s standard metadata: - Open Graph tags (`og:title`, `og:description`, `og:image`) - Twitter Card tags as a fallback - Standard HTML `` and first suitable `<img>` if neither is present For the best-looking preview on your own domain, publish proper Open Graph tags on every page you send. Pages behind auth, intranet URLs, and pages that block the Linq preview fetcher will render as plain-link fallbacks. ## When to use link parts vs. text + URL | Goal | Use | | ------------------------------------------- | ----------------------------------------------------------------- | | Rich card with title and image | **Link part** (this guide) | | Short message that happens to contain a URL | Text part — iMessage auto-previews the URL inline | | Multiple URLs in one message | Multiple messages, one link part each (link parts can’t be mixed) | ## Related - [Sending Messages — message parts](/guides/messaging/sending-messages#message-parts/index.md) - [Protocol Selection](/guides/messaging/protocol-selection/index.md) — which services render rich previews - [Error 1004 — Invalid message content](/error/codes/1xxx/1004/index.md) - [API Reference: Send Message](/api/resources/chats/subresources/messages/methods/send/index.md) --- # Sending Messages URL: https://docs.linqapp.com/guides/messaging/sending-messages/ The Linq API lets you send messages containing text, media, or a mix of both across iMessage, RCS, and SMS. ## Starting a conversation To message a new recipient, create a chat with an initial message. The API creates the conversation and sends the message in a single request: - [cURL](#tab-panel-113) - [TypeScript](#tab-panel-114) - [Python](#tab-panel-115) - [Go](#tab-panel-116) 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` must be a phone number assigned to your account. - `to` accepts an array — one recipient for a direct message, multiple for a [group chat](/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](/guides/messaging/rich-link-previews/index.md) using the returned chat ID. See the [Create Chat](/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](/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`, or `link`. You can mix text and media in a single message; link parts must 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" } ``` **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 - Consecutive text parts are **not allowed** — separate them with media or send as individual messages See [Attachments](/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](/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. ## Replying to messages Thread messages by referencing a specific message ID: - [cURL](#tab-panel-117) - [TypeScript](#tab-panel-118) - [Python](#tab-panel-119) - [Go](#tab-panel-120) 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](/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](/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](/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-121) - [TypeScript](#tab-panel-122) - [Python](#tab-panel-123) - [Go](#tab-panel-124) 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](/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`](/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](/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](/api/resources/messages/methods/delete/index.md). --- # Voice Memos URL: https://docs.linqapp.com/guides/messaging/voice-memos/ Voice memos are sent through a dedicated endpoint. On **iMessage**, they render with the native inline audio player — the voice-memo bubble you get when recording in the Messages app. On **RCS** and **SMS**, this endpoint falls back to a regular audio attachment, which is the same result you’d get from sending the audio as a media part via the [standard send-message flow](/guides/messaging/sending-messages#message-parts/index.md). In practice the endpoint is useful when you specifically want the iMessage voice-memo affordance for iMessage-capable recipients; for RCS/SMS delivery you can use either path (see [Protocol Selection](/guides/messaging/protocol-selection/index.md)). ## Sending a voice memo Pass either a publicly-accessible HTTPS `voice_memo_url` or a pre-uploaded `attachment_id` (see [Attachments](/guides/messaging/attachments/index.md)). See the [Send Voice Memo API reference](/api/resources/chats/methods/send_voicememo/index.md) for the full endpoint specification. ### With a URL - [cURL](#tab-panel-125) - [TypeScript](#tab-panel-126) - [Python](#tab-panel-127) - [Go](#tab-panel-128) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/voicememo \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "voice_memo_url": "https://example.com/voice-memo.m4a" }' ``` ``` await client.chats.sendVoicememo({chatId}, { voice_memo_url: "https://example.com/voice-memo.m4a", }); ``` ``` client.chats.send_voicememo( {chat_id}, voice_memo_url="https://example.com/voice-memo.m4a", ) ``` ``` client.Chats.SendVoicememo(context.TODO(), {chatId}, linq.ChatSendVoicememoParams{ VoiceMemoUrl: linq.F("https://example.com/voice-memo.m4a"), }) ``` ### With a pre-uploaded `attachment_id` - [cURL](#tab-panel-129) - [TypeScript](#tab-panel-130) - [Python](#tab-panel-131) - [Go](#tab-panel-132) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/voicememo \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "attachment_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` ``` await client.chats.sendVoicememo({chatId}, { attachment_id: "550e8400-e29b-41d4-a716-446655440000", }); ``` ``` client.chats.send_voicememo( {chat_id}, attachment_id="550e8400-e29b-41d4-a716-446655440000", ) ``` ``` client.Chats.SendVoicememo(context.TODO(), {chatId}, linq.ChatSendVoicememoParams{ AttachmentId: linq.F("550e8400-e29b-41d4-a716-446655440000"), }) ``` ## Supported audio formats | Format | MIME type | | ------ | ---------------------------- | | MP3 | `audio/mpeg` | | M4A | `audio/x-m4a`, `audio/mp4` | | AAC | `audio/aac` | | CAF | `audio/x-caf` | | WAV | `audio/wav` | | AIFF | `audio/aiff`, `audio/x-aiff` | | AMR | `audio/amr` | ## Notes - **iMessage-specific affordance** — The inline voice-memo bubble is iMessage-only. On RCS and SMS this endpoint delivers the audio as a regular attachment, equivalent to sending a media part via the [standard send-message flow](/guides/messaging/sending-messages#message-parts/index.md) — use whichever path is more convenient for those recipients. - **HTTPS required** — The source URL must be publicly reachable with a valid TLS certificate. The API downloads the file before sending. - **File size** — The 10 MB direct-URL limit applies. Larger files should be hosted on a CDN you control. - **Webhooks** — Voice memo delivery is confirmed via the standard `message.sent` / `message.delivered` / `message.failed` events. See [Webhooks](/guides/webhooks/index.md). --- # Phone Numbers URL: https://docs.linqapp.com/guides/phone-numbers/ Phone Numbers represent the phone numbers assigned to your partner account. Use the list phone numbers endpoint to discover which phone numbers are available for sending messages. When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers in the `from` field. “Line” and “phone number” mean the same thing These guides use **line** in prose (the natural term for a single provisioned number) and **phone number** in API fields (`phone_number`) and feature names (**Phone Reputation**). They refer to the same thing. ## List your phone numbers See the [List Phone Numbers API reference](/api/resources/phone_numbers/methods/list/index.md) for the full endpoint specification. - [cURL](#tab-panel-133) - [TypeScript](#tab-panel-134) - [Python](#tab-panel-135) - [Go](#tab-panel-136) Terminal window ``` curl https://api.linqapp.com/api/partner/v3/phone_numbers \ -H "Authorization: Bearer $LINQ_API_KEY" ``` ``` await client.phoneNumbers.list(); ``` ``` client.phoneNumbers.list() ``` ``` client.PhoneNumbers.List(context.TODO()) ``` ## Provisioning Numbers are provisioned by your Linq representative — there is no self-serve create or delete endpoint on the V3 API. To add or release a line, contact support with the details of the line you want changed. ## Status changes Numbers carry two independent fields you can react to: - **`status`** — the line’s current sending state. - **`ACTIVE`** — sending and receiving normally. - **`FLAGGED`** — a service flag has degraded the number’s ability to send. New messages on a flagged line may fail with delivery errors. - **`reputation`** — line-level prediction of where the line is heading based on the patterns in its conversations *and* its overall messaging activity. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation/index.md) for the `HEALTHY` / `AT_RISK` / `CRITICAL` values and how to react. (`health_status` is a **deprecated** alias that returns the same value and will be removed.) The [`phone_number.status_updated`](/guides/webhooks/events#phone-number-events/index.md) webhook fires when **either** field changes, and every payload carries both pairs (`previous_status` / `new_status` and `previous_reputation` / `new_reputation`, plus the deprecated `previous_health_status` / `new_health_status` aliases), so a single subscription covers both signals. ``` { "phone_number": "+12025551234", "previous_status": "ACTIVE", "new_status": "FLAGGED", "previous_reputation": "AT_RISK", "new_reputation": "CRITICAL", "previous_health_status": "AT_RISK", "new_health_status": "CRITICAL", "changed_at": "2026-02-18T18:35:05.000Z" } ``` You can also enable **Flagged-number Slack notifications** in the [API Tooling](https://dashboard.linqapp.com/api-tooling/phone-numbers) settings to get a Slack message in your linked partner channel whenever a number’s `status` changes. **Recommended handling:** - Page on-call when a production line transitions to `FLAGGED`. - Pause outbound sends on the affected line until it returns to `ACTIVE`. - Contact your Linq representative for remediation. - For `reputation` transitions to `AT_RISK` or `CRITICAL`, follow the playbook in the [Phone Reputation guide](/guides/phone-numbers/phone-reputation/index.md) — typically slow the line’s pace before the line ends up `FLAGGED`. ## Related - [Key Concepts: Phone Numbers](/getting-started/key-concepts#phone-numbers/index.md) - [Phone Reputation](/guides/phone-numbers/phone-reputation/index.md) — line-level reputation scoring - [Webhooks: Phone number events](/guides/webhooks/events#phone-number-events/index.md) - [API Reference: Phone Numbers](/api/resources/phone_numbers/index.md) --- # Phone Reputation URL: https://docs.linqapp.com/guides/phone-numbers/phone-reputation/ Note This feature is currently in beta and may be inaccurate at times. We are actively improving our reputation models. Expect additional engagement and activity signals as the scoring gets smarter. Caution The field is now called `reputation`. `health_status` is a **deprecated** alias that returns the same value and will be removed — migrate to `reputation`. Every phone line carries a `reputation` — a line-level read on whether the line as a whole is in good standing. It’s the line-wide companion to [chat health](/guides/chats/chat-health/index.md), but it is **not** simply a sum of it. Phone reputation draws on two kinds of signals: - **Patterns across the conversations on the line** — engagement and deliverability across the line’s chats, in aggregate. This is the larger driver, and it’s closely related to [chat health](/guides/chats/chat-health/index.md). - **Line-level activity that no single conversation reveals** — how many different recipients the line messages in a short window, how many brand-new conversations it starts in a day, and its overall send pace. You’ll see `reputation` on each number returned by [`GET /v3/phone_numbers`](/api/resources/phone_numbers/methods/list/index.md). It’s a prediction of where the line is heading — treat it as a pre-send gate, not a report. ## How to use it - **[`GET /v3/phone_numbers`](/api/resources/phone_numbers/methods/list/index.md)** — every line in the list response carries its current `reputation`. - **[`phone_number.status_updated`](/guides/webhooks/events#phone-number-events/index.md) webhook** — fires when `reputation` changes. The payload carries `previous_reputation` and `new_reputation` (and the deprecated `previous_health_status` / `new_health_status` aliases), so you can react the moment a line moves to `AT_RISK` or `CRITICAL`. Check `reputation` shape and value before you queue outbound messages for a line — the same pre-send gate pattern as chat health, one level up. Use it to route work toward your healthiest lines: if a line is `CRITICAL`, onboard new recipients onto a `HEALTHY` line instead and let the `CRITICAL` one recover before you send more on it. ``` const { phone_numbers } = await client.phoneNumbers.list(); for (const pn of phone_numbers) { switch (pn.reputation.status) { case 'HEALTHY': break; // send normally, safe to onboard new recipients case 'AT_RISK': reviewLine(pn); break; // slow the pace, check chat health case 'CRITICAL': pauseLine(pn); break; // stop outbound, route new recipients elsewhere } } ``` Acting on the status before each send is what turns the signal into delivery improvement — ultimately leading to a healthier line. ## Statuses | status | What it means | What to do | | ----------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------- | | [`HEALTHY`](#healthy) | The line is in good standing. | Send normally. | | [`AT_RISK`](#at-risk) | The line’s overall engagement is trending down. | Slow the line’s send pace and use chat health to find conversations to fix. | | [`CRITICAL`](#critical) | Strong signals that messages from this line aren’t landing well. | Pause outbound on the line until it recovers. | ### HEALTHY The line looks like it’s operating normally. Engagement and deliverability across its conversations look good, and its overall activity is within a normal range. No action needed. ### []()AT\_RISK One or more soft signals suggest the line as a whole is heading in the wrong direction. Common drivers fall into two groups: - **Conversation patterns.** Engagement is low across many of the line’s chats. [Chat health](/guides/chats/chat-health/index.md) is the best place to see this per-conversation. - **Line-level activity.** The line is reaching a large number of different recipients in a short window, starting many brand-new conversations in a single day, or its overall send volume spiked — patterns that look risky at the line level even when individual chats are fine. `AT_RISK` is a *warning*, not a hard stop. Recommended playbook: 1. **Check chat health.** Pull [chat health](/guides/chats/chat-health/index.md) for the line’s conversations to find the `AT_RISK` and `CRITICAL` chats, and fix those first. 2. **Slow the line’s overall pace.** Reduce total send volume until signals recover. 3. **Bias outbound toward messages that elicit replies.** Questions and follow-ups outperform broadcasts. 4. **Spread activity out.** Avoid contacting many new recipients or opening many new conversations in a single burst. 5. **Vary your content.** Repeated near-identical messages across the line amplify the signal. Watch for the line moving back to `HEALTHY` (good) or down to `CRITICAL` (act fast). ### CRITICAL Strong signals that messages from this line aren’t reaching recipients the way you expect. Continuing to send is unlikely to help and may make the situation worse. Recommended action: pause outbound on the line. Re-engage only after it returns to `HEALTHY`. ## Scenarios | Scenario | Likely status | | ---------------------------------------------------------------- | ----------------------- | | Send volume with engaging reply rates from users | `HEALTHY` | | Rapid increase in new conversations with little to no engagement | `AT_RISK` or `CRITICAL` | | Many of the line’s chats are `AT_RISK` or `CRITICAL` | `AT_RISK` or `CRITICAL` | ## Phone reputation and chat health Chat health and phone reputation are related but answer different questions: - **[Chat health](/guides/chats/chat-health/index.md)** scores a single conversation — is *this* recipient engaging, are messages landing. - **Phone reputation** scores the line — both the aggregate of its conversations *and* line-level activity that no single conversation reveals, like how broadly and how fast the line is messaging. ## Tips - New lines start as `HEALTHY` and move to `AT_RISK` or `CRITICAL` as signals warrant. ## FAQ **Does phone reputation read message content or store any PII?** No. Phone reputation scoring runs on anonymous, aggregate signals — line-level volume, send/receive ratios, recipient counts, conversation pace, and similar metadata. Linq does not collect or store message content or other PII to compute `reputation` for phone numbers. **Why is there no `OPTED_OUT` on phone reputation?** Unlike chat health, phone reputation has **no `OPTED_OUT` status**. Opt-out is a per-recipient signal scoped to a single conversation — it can’t apply to a whole line. You’ll only ever see `OPTED_OUT` on [chat health](/guides/chats/chat-health#opted-out/index.md). **Why are most of my high-volume lines `AT_RISK`?** Volume alone shouldn’t cause this. Check whether user reply rates dropped on those lines as they scaled — phone reputation weighs engagement relative to volume, not necessarily volume on its own; although, too many new chats sent at once within a short period can cause `AT_RISK` signals. **Is `AT_RISK` reversible?** Yes. The status reflects recent behavior. Improving send pace and engagement on the line returns it to `HEALTHY` as new signals come in. **Should I migrate users off an `AT_RISK` line?** No — fix the line. Migrating users off carries the same underlying sending pattern to a new line and spreads the issue rather than solving it. Use [chat health](/guides/chats/chat-health/index.md) to find the conversations dragging the line down, address those, and let the line recover. ## Related - [Phone Numbers](/guides/phone-numbers/index.md) — discover and monitor your lines - [Chat Health](/guides/chats/chat-health/index.md) — per-conversation health scoring - [API Reference: List Phone Numbers](/api/resources/phone_numbers/methods/list/index.md) --- # Debugging URL: https://docs.linqapp.com/guides/platform/debugging/ Every API response and webhook payload includes a trace ID for end-to-end debugging. This guide covers how to use trace IDs and troubleshoot common issues. See [Error Codes](/error/index.md) for the complete error reference. ## Trace IDs Every API response includes a trace ID in the `X-Trace-ID` response header, following the [W3C Trace Context](https://www.w3.org/TR/trace-context/) standard. Error responses and webhook payloads also include `trace_id` in the response body. Terminal window ``` # The response includes the trace ID header < X-Trace-ID: 2eff5df5c6f688733c007523c4d61cd9 ``` ``` { "success": false, "error": { "status": 400, "code": 1001, "message": "Missing required field", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1001/" }, "trace_id": "2eff5df5c6f688733c007523c4d61cd9" } ``` ## W3C Trace Context The trace ID is the 32-character hex trace-id segment of the W3C `traceparent` header. Its structure is: ``` traceparent: 00-<trace-id>-<parent-id>-<flags> │ │ │ └─ 2-hex flags (e.g. 01 for sampled) │ │ └─ 16-hex span/parent ID │ └─ 32-hex trace ID (returned as X-Trace-ID) └─ version (currently 00) ``` The `trace_id` field in error bodies and webhook envelopes is the same 32-hex value. You can feed it straight into any OpenTelemetry-compatible tracing backend (Datadog, Honeycomb, Jaeger, Grafana Tempo, etc.) to join your spans with Linq’s internal spans — ask support to enable cross-account trace sharing if you need to inspect Linq-side spans. **Inbound headers are ignored.** Per W3C security guidance for public APIs, the Linq API generates a fresh trace context for every request. Any client-supplied `traceparent` or `tracestate` headers are discarded and replaced. This prevents forged trace IDs and trace-ID collision across tenants. ## Request correlation To correlate an API request with your own records: - **Store the returned `X-Trace-ID`** and map it to your internal request/job ID - **Log both together** so you can cross-reference during support tickets - **Capture it on webhook receipt too** — the same trace ID flows through downstream `message.sent` / `message.delivered` events Use `curl -i` (or the equivalent raw-response accessor in your SDK — `response._response.headers` in TypeScript, `client.<resource>.with_raw_response.<method>` in Python) to read the `X-Trace-ID` header from any response: Terminal window ``` curl -i -X POST https://api.linqapp.com/api/partner/v3/chats \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from":"+12223334444","to":["+15556667777"],"message":{"parts":[{"type":"text","value":"Hello!"}]}}' \ | grep -i '^x-trace-id:' ``` ## Async processing and webhook correlation Many operations are processed asynchronously after the API responds. The initial HTTP response tells you the request was accepted; the final outcome arrives via [webhooks](/guides/webhooks/index.md). The `trace_id` threads the full request lifecycle: 1. You call `POST /v3/chats/{id}/messages` → response body and `X-Trace-ID` header carry `trace_id: abc123...` 2. The message is queued and dispatched to Apple/carrier networks 3. You receive a [`message.sent`](/guides/webhooks/events#message-events/index.md) webhook whose envelope `trace_id` matches `abc123...` 4. On delivery, a [`message.delivered`](/guides/webhooks/events#message-events/index.md) webhook — same `trace_id` 5. On read, [`message.read`](/guides/webhooks/events#message-events/index.md) — same `trace_id` 6. On failure, a [`message.failed`](/guides/webhooks/events#message-events/index.md) webhook — same `trace_id` plus the final `code` / `message` **Recommended pattern:** store the `trace_id` alongside the message in your database at step 1, then key webhook-handler updates off `trace_id` (or `event_id` for deduplication) so late-arriving events always land on the correct record. > **Note:** Webhook envelope `trace_id` is always present. The inner `data` object may also echo `message_id`, `chat_id`, and `event_id` for more granular joins. Use `event_id` for deduplication, `trace_id` for end-to-end correlation. ## Common issues | Symptom | Likely cause | Solution | | -------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `401 Unauthorized` | Missing or invalid bearer token | Check `Authorization: Bearer` header — see [Authentication](/getting-started/authentication/index.md) | | `400 E.164 format` | Phone number format wrong | Use `+` prefix with country code: `+12223334444` — see [error 1002](/error/codes/1xxx/1002/index.md) | | `403 Phone number denied` | Phone not assigned to your account | Verify phone assignment with your Linq representative | | `409 Chat still creating` | Duplicate concurrent create requests | Wait and retry, or use [idempotency keys](/guides/messaging/sending-messages#idempotency/index.md) | | `429 Rate limit` | Per-pair cap, sandbox daily cap, or capability check limit | Wait the `Retry-After` interval, then check for retry loops or missing caching — see [Rate Limits](/guides/platform/rate-limits/index.md) | | `5xx Infrastructure` | Transient server error | Retry with exponential backoff — see [3xxx server errors](/error#3xxx--server-errors/index.md) | | Webhook not received | Endpoint unreachable or returning non-2xx | Check endpoint URL, SSL cert, and firewall rules — see [Webhooks](/guides/webhooks/index.md) | | No `message.delivered` / `message.read` after send | Message fell back to SMS/MMS | SMS and MMS don’t support delivery or read receipts — see [Protocol capabilities](/guides/messaging/protocol-selection#protocol-capabilities/index.md) | | Duplicate webhooks | At-least-once delivery | Deduplicate using `event_id` — see [Webhook delivery](/guides/webhooks#delivery-guarantees/index.md) | ## Reporting issues When contacting Linq support, always include: 1. The **trace ID** from the API response 2. The **timestamp** of the request 3. The **endpoint** you called 4. The **response** you received (status code and body) This allows the support team to look up the exact request in their systems. --- # Rate Limits URL: https://docs.linqapp.com/guides/platform/rate-limits/ The Linq API enforces rate limits to protect Apple’s ecosystem and prevent automated feedback loops between bots. ## Recommended daily volume We recommend limiting message volume to a combined total of up to **7,000 inbound and outbound messages per day per line**. Volume north of the recommended range has the potential to degrade performance. There are no hard caps on daily volume (excluding [sandbox accounts](https://dashboard.linqapp.com/sandbox-signup/)). These recommendations are meant to optimize the performance of your lines. We recommend you control volume via onboarding flows you build on your end. ## Per-phone-pair rate limit The API enforces a rate limit of **30 messages per 60-second window** for each unique sender–recipient pair. This is designed to prevent automated feedback loops where two numbers (i.e. bots) rapidly exchange messages. When exceeded, the API returns **HTTP `429`** with a `Retry-After` header (seconds until reset) and [error code `1007`](/error/codes/1xxx/1007/index.md). The same value is also available as `retry_after` in the error body. Hitting this limit almost always means a bug — check for retry loops, duplicate sends, or webhook handlers that inadvertently reply to themselves. ## Sandbox limits [Sandbox accounts](https://dashboard.linqapp.com/sandbox-signup/) are the exception to the “no hard caps” rule: - **100 messages per day** — Resets at midnight UTC ## Capability check rate limit [Capability check](/guides/chats/capability-checks/index.md) endpoints (`/v3/capability/check_imessage` and `/v3/capability/check_rcs`) are also rate-limited to prevent abuse (e.g. using them to enumerate which numbers are on iMessage). Excessive calls return the same **HTTP `429`** with `Retry-After` and [error code `1007`](/error/codes/1xxx/1007/index.md). Capability checks are limited to 1 per 10 seconds. Cache results briefly (minutes, not days) rather than re-checking the same address on every send — capability is stable over short windows and the cache reduces your risk of hitting this limit. ## Rate limit response When you hit a rate limit, the API returns HTTP `429 Too Many Requests` with a `Retry-After` header and this body: ``` { "success": false, "error": { "status": 429, "code": 1007, "message": "Rate limited. Try again in 45 seconds.", "retry_after": 45, "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1007/" }, "trace_id": "trace_abc123def456" } ``` The `retry_after` field mirrors the `Retry-After` header and is only present on `429` responses. See [Error Codes](/error/index.md) for the full envelope shape used by all error responses. ## Handling rate limits On a `429` response, prefer the `Retry-After` header when present; fall back to exponential backoff (e.g. 1s → 2s → 4s) otherwise. Do not retry on `429` more than a few times — keep an upper bound and surface the failure if you keep hitting the wall. > **Tip:** The official [SDKs](/getting-started/sdks/index.md) handle this automatically with exponential backoff. You only need custom retry logic if you’re calling the API directly. ## Best practices - **Queue messages** — Use a message queue to control send rate rather than sending as fast as possible - **Monitor usage** — Track your daily message count to avoid hitting limits unexpectedly - **Batch wisely** — Space out bulk sends rather than sending all at once - **Use idempotency keys** — Include an `idempotency_key` field in the message body on retries to prevent duplicates (see [Sending Messages](/guides/messaging/sending-messages#idempotency/index.md)) ## Other error codes Rate limits aren’t the only reason requests can fail. See [Error Codes](/error/index.md) for the complete reference, including infrastructure errors (3xxx) that may also require retry logic. See the [Send Message](/api/resources/chats/subresources/messages/methods/send/index.md) and [Create Chat](/api/resources/chats/methods/create/index.md) API references for endpoint details. --- # Examples URL: https://docs.linqapp.com/guides/resources/example/ These open-source example apps demonstrate what you can build with the Linq Partner API. Each repo is ready to clone, configure with your API keys, and run. See [Client SDKs](/getting-started/sdks/index.md) for SDK installation and [Authentication](/getting-started/authentication/index.md) for API key setup. ## AI Agent A complete, open-source example of a Claude-powered AI agent running on iMessage via the Linq API. Fork it, customize it, and deploy your own in minutes. - **Stack:** TypeScript, Claude API, Linq API - **Features:** Conversational AI, [webhook handling](/guides/webhooks/index.md), [message threading](/guides/messaging/sending-messages#replying-to-messages/index.md) [View on GitHub](https://github.com/linq-team/ai-agent-example) | [Use Case](https://linqapp.com/s/use-cases/ai-agent) ## Bookings Agent A booking agent powered by Claude that searches restaurants, books tables, and manages reservations through Resy — all from iMessage, built on Linq. - **Stack:** TypeScript, Claude API, Resy API, Linq API - **Features:** Tool use, restaurant search, reservation management [View on GitHub](https://github.com/linq-team/linq-resy-agent) | [Use Case](https://linqapp.com/s/use-cases/bookings-agent) ## Markets Agent Kai is an open-source trading agent with 14 Claude tools, RSA-PSS signed Kalshi API calls, and rich iMessage features. Search markets, place trades, and track your portfolio entirely via text. - **Stack:** TypeScript, Claude API, Kalshi API, Linq API - **Features:** Market search, trade execution, portfolio tracking, RSA-PSS signing [View on GitHub](https://github.com/linq-team/kalshi-agent) | [Use Case](https://linqapp.com/s/use-cases/kalshi-agent) ## Granola Agent A meeting-notes agent that syncs with your Granola account and lets you search and summarize notes over iMessage. Ask for a recap of last Tuesday’s standup and get it back as a text. - **Stack:** TypeScript, Claude API, Granola API, Linq API - **Features:** Account sync, note search, on-demand summaries [View on GitHub](https://github.com/linq-team/granola-agent) ## Claude Code over iMessage A reference channel that exposes Claude Code through iMessage using the Linq CLI. Drive Claude Code sessions — file edits, terminal commands, PR reviews — from your phone. - **Stack:** Claude Code, Linq CLI, Linq API - **Features:** Rapid agent deployment, terminal + repo access from iMessage [View on GitHub](https://github.com/linq-team/claude-code-imessage-channel) ## More examples See the full list at [linqapp.com/s/example-apps](https://linqapp.com/s/example-apps), or browse the [Linq GitHub organization](https://github.com/linq-team) for additional projects and integrations. --- # FAQ URL: https://docs.linqapp.com/guides/resources/faq/ ## General Where can I access the API toolkit? The API toolkit lives in the [Linq dashboard](https://dashboard.linqapp.com/api-tooling) — look for the code icon. From there you can manage API keys, browse documentation, configure webhook subscriptions, view delivery logs, and check line status. How do I debug API issues? Every API response includes a `trace_id` — include it when contacting support to speed up diagnosis. See [Debugging](/guides/platform/debugging/index.md) for a full troubleshooting guide. How do I resolve an error I'm getting from the API? Every error response includes a `doc_url` field that links directly to the reference page for that error code. Open it to see the description, common causes, and resolution steps. See [Error Codes](/error/index.md) for the full reference. Do you have read-only API keys? Not currently — all API keys have full read/write access. How do I rotate my API key? Generate a new token from the Linq dashboard at [dashboard.linqapp.com/api-tooling](https://dashboard.linqapp.com/api-tooling) under **API → Overview → Generate new token**. What's the best place to monitor usage and health of my lines? Check the analytics page in the [Linq dashboard](https://dashboard.linqapp.com/api-tooling/phone-numbers) for usage, and the phone numbers section for operational status. For programmatic monitoring, subscribe to the `phone_number.status_updated` webhook event. Where can I view a list of my phone lines? To see all your phone lines, call `GET /api/partner/v3/phone_numbers` or browse them in the [Linq dashboard](https://dashboard.linqapp.com/api-tooling/phone-numbers). Are there best practices for messaging with the Linq API? Yes — see the [Best Practices](/getting-started/best-practices/index.md) guide for recommended patterns around messaging flow, contact card sharing, opt-out handling, and keeping lines in good standing. What messaging protocols does Linq support? Linq supports **iMessage**, **RCS**, and **SMS/MMS**. The API automatically selects the best protocol based on recipient capabilities, or you can explicitly choose one with the `preferred_service` parameter. See [Protocol Selection](/guides/messaging/protocol-selection/index.md) for details. Is there a sandbox or test environment? Yes! Sign up for a [sandbox account](https://dashboard.linqapp.com/sandbox-signup/) to get started. Sandbox accounts have a 100 messages/day limit — see [Rate Limits](/guides/platform/rate-limits#sandbox-limits/index.md) for details. We recommend using separate API tokens for development and production. How do I migrate from V2 to V3? V3 introduces a redesigned resource model, multi-part messages, event-based webhooks, and a new base path (`/api/partner/v3/`). Generate a V3 API token from the Linq dashboard at [dashboard.linqapp.com/api-tooling](https://dashboard.linqapp.com/api-tooling) under **API → Overview → Generate new token**. See the [V2 to V3 Migration Guide](/guides/resources/migration-v2-to-v3/index.md) for a full breakdown of what changed. Can I use V2 and V3 simultaneously? Yes. V2 and V3 operate independently with the same token but with different endpoints. You can run both during a migration period, but messages sent via one version won’t appear in the other. What SDKs are available? Official SDKs are available for **TypeScript/Node.js**, **Python**, and **Go**. All SDKs include automatic retries, type safety, pagination helpers, and idempotency support. See [Client SDKs](/getting-started/sdks/index.md) for installation instructions. ## Share Contact Card How can I get the Share Contact Card feature enabled? Call [`POST /api/partner/v3/contact_card`](/api/resources/contact_card/methods/create/index.md) to configure your contact card — this is a one-time setup per phone number. You can also manage this in the [Linq dashboard](https://dashboard.linqapp.com/contact-cards). Once configured, send a message to the recipient first, then [share the contact card](/guides/chats/share-contact-card/index.md) — it will only appear if an outbound message already exists in the chat. **Note:** - Profile images must be 1:1 aspect ratio, minimum 200×200 px - If a number is replaced, you’ll need to set up the contact card again for the new number How do I use Share Contact Card? Share the contact card after your first outbound message to a recipient — at most once per day. See [Share Contact Card](/guides/chats/share-contact-card/index.md) for details. If a user shares their contact card with me, can I add them? `.vcf` file attachments can be saved, but Apple contact card data cannot currently be read or saved via the API. How can I share contact info with non-iPhone users? Send a `.vcf` file attachment containing the contact’s name, picture, phone number, and any other details. See [Attachments](/guides/messaging/attachments/index.md) for how to send files. Does Share Contact Card work with non-iPhone users? No — Share Contact Card is iMessage only and will not appear for Android or other non-iPhone users. What does the Share Contact Card POST request do? It pushes an Apple contact card via iMessage, showing a popup at the top of the recipient’s thread to add the contact. See [Share Contact Card](/guides/chats/share-contact-card/index.md) for details. ## Messages How can I check what service my recipients are on? Use `POST /api/partner/v3/capability/check_imessage` or `POST /api/partner/v3/capability/check_rcs` to check recipient capabilities before sending. The service used is also reported in `message.sent` and `message.received` webhook events. What support exists for threaded replies? Use the `reply_to` parameter with a `message_id` and an optional `part_index` to reply to a specific part of a multi-part message. See [Sending Messages](/guides/messaging/sending-messages#replying-to-messages/index.md) for details. Why am I not receiving message.read events even after a recipient responds? Read receipts depend on the recipient’s device settings — users can disable read receipts in iMessage. Group chats do not support read receipts or delivery receipts — you’ll only receive `message.sent`. Why am I not receiving message.delivered events for some numbers? Delivery receipts are only available for iMessage and RCS direct messages. Group chats and SMS-only recipients do not emit `message.delivered` — you’ll receive `message.sent` only. How do message reactions (tapbacks) work? Supported reactions are: `love`, `like`, `dislike`, `laugh`, `emphasize`, and `question`. You can also send custom emoji reactions using `type: custom` with a `custom_emoji` field. Since reactions are async, you’ll receive webhook events for both inbound reactions and as confirmation when your own are added or removed. See [Reactions](/guides/messaging/reactions/index.md) for details. What's your default data retention policy? Messages are deleted from the Linq system every 24 hours, and from iCloud every 30 days (Apple’s minimum retention period). Can I delete messages? Yes — call [`DELETE /v3/messages/{messageId}`](/api/resources/messages/methods/delete/index.md) to remove a message from Linq’s records. Note: this does **not** unsend the message — recipients will still see it. Can I edit messages? Yes, you can edit messages using [`PATCH /v3/messages/{messageId}`](/api/resources/messages/methods/update/index.md), which will amend the contents of the message on your and the recipient’s end. A message can be edited up to 5 times within 15 minutes from when it was originally sent. You’ll receive a `message.edited` webhook as confirmation. Only supported in webhook version `2026-02-03` and later. How can I prevent duplicate messages? Use the `idempotency_key` parameter when sending a message. If you send the same request twice with the same key, the API returns the original message instead of creating a duplicate. See [Idempotency](/guides/messaging/sending-messages/#idempotency/index.md) for details. Do you support sending App Clips? Yes, simply send the App Clip link on its own and it will display normally for the recipient. Can I send OTP codes through my Linq line? Linq numbers are P2P iMessage numbers, and are not explicitly designed to deliver that category of messaging. To ensure consistent delivery of OTP codes, we recommend you utilize a traditional SMS provider to handle that function. How do I send a voice memo? Call `POST /api/partner/v3/chats/{chatId}/voicememo` with the audio file. Supported formats: MP3, M4A, AAC, CAF, WAV, AIFF, AMR. Max size: 10MB. Send it alone — voice memos cannot be combined with other message parts. See [Voice Memos](/guides/messaging/voice-memos/index.md) for details. How can I make URLs unfurl into rich previews? Set the part `type` to `link` and send the link alone with no other parts. The URL must include a scheme (`http://`, `https://`, or `ftp://`). See [Rich Link Previews](/guides/messaging/rich-link-previews/index.md) for details. What types of files can I send? You can send images, videos, audio, documents, contacts, and calendar events — either via URL on a `media` part or by pre-uploading via [`POST /api/partner/v3/attachments`](/api/resources/attachments/methods/create/index.md) and referencing the returned `attachment_id`. Make sure to add `cdn.linqapp.com` to any domain allowlists. See [supported file types](/guides/messaging/attachments/#supported-file-types/index.md) for details. ## Chats Can I leave a group chat? Yes — call [`POST /api/partner/v3/chats/{chatId}/leave`](/api/resources/chats/methods/leave_chat/index.md). The group must have at least 3 participants remaining after you leave. Once you leave, you can no longer access the chat unless an active participant adds you back. Recreating a chat with the same set of participants will create a new, separate chat. See [Group Chats](/guides/chats/group-chats/index.md) for details. Can I add or remove participants? Yes — add participants via [`POST /api/partner/v3/chats/{chatId}/participants`](/api/resources/chats/subresources/participants/methods/add/index.md). The chat must already have 3+ participants, and the new participant must support the same messaging service. Remove participants via `DELETE` — at least 3 participants must remain. You cannot remove yourself from the group chat — use [`POST /leave`](/api/resources/chats/methods/leave_chat/index.md) instead. See [Group Chats](/guides/chats/group-chats/index.md) for details. Why does a removed participant still appear in the chat handles? When a participant is removed, they remain in the chat’s handles list with `status: removed` and a `left_at` timestamp, but can no longer send or receive messages in the chat. Is adding a user to a group chat considered outbound first communication? Yes — adding a user to a group chat counts as outbound activity regardless of who sends the first message. What happens to the Chat ID if a participant is added or removed? The Chat ID always stays the same when participants are added or removed — it is stable for the lifetime of the chat. ## Typing Indicators Do typing indicators work in group chats? No — typing indicators are not currently supported in group chats. Why isn't my typing indicator showing despite a 204 response? Typing indicators are best-effort — a `204` means the request was accepted, not that it was delivered. The chat must have had activity within the last 5 minutes, and group chats are not supported. See [Typing Indicators](/guides/chats/typing-indicators/index.md) for details. ## Voice Calls How can I make outbound calls? There is no native outbound calling support. You can use a voice-capable service with number masking and set your Linq number as the masked caller ID. How can I receive inbound voice calls? You cannot pick up calls to your Linq number directly. You can forward inbound calls via `PUT /api/partner/v2/phone_number/{id}` (V2 API). ## Webhooks Where can I set up webhook subscriptions? Create and manage webhook subscriptions via [`POST /api/partner/v3/webhook-subscriptions`](/api/resources/webhook_subscriptions/methods/create/index.md), or through the [Linq dashboard](https://dashboard.linqapp.com/api-tooling/webhooks). How do I verify webhook signatures? All webhooks are signed with HMAC-SHA256 using your webhook signing secret. The signature is computed over `{timestamp}.{payload}` and sent in the `X-Webhook-Signature` header. See [Signature Verification](/guides/webhooks/#signature-verification/index.md) for step-by-step instructions and SDK examples. What events can I subscribe to? Linq delivers events across messages, reactions, chats, participants, and phone numbers. You choose which events to subscribe to when creating a webhook subscription. You can also call [`GET /api/partner/v3/webhook_events`](/api/resources/webhook_events/methods/list/index.md) to fetch all available event types programmatically. See [Webhook Event Types](/guides/webhooks/events/index.md) for the full list and a representative payload per event. Can webhooks be configured on a per-number basis? Yes — when creating a subscription, pass an optional `phone_numbers` filter to limit delivery to specific numbers. By default, a subscription fires for all numbers in your environment. See [Filtering by phone number](/guides/webhooks/subscriptions/#filtering-by-phone-number/index.md) for details. What is your webhook retry policy? Linq retries failed webhook deliveries up to 10 times over approximately 25 minutes per endpoint. Retries are triggered by HTTP `5xx` responses, HTTP `429`, connection timeouts over 10 seconds, or connection refused / network errors. See [Delivery Guarantees](/guides/webhooks/#delivery-guarantees/index.md) for details. How should I handle duplicate webhooks? Use the `event_id` field to deduplicate events. The same event may arrive multiple times during retries, so your webhook handler should be idempotent — processing the same event twice should have no side effects. We recommend: - Store processed `event_id` values (e.g., in Redis or your database) - Check if an incoming `event_id` has already been processed - Skip processing if it’s a duplicate Will Linq send multiple webhooks quickly without waiting for a 200 response? Yes — Linq sends webhook events regardless of whether your server has responded to previous deliveries. Log and process events asynchronously to avoid timeouts. If I miss webhooks due to an outage, how can I retrieve missing data? Call [`GET /api/partner/v3/chats/{chatId}`](/api/resources/chats/methods/retrieve/index.md) to pull missed data for a specific chat, or contact your Linq representative to request a manual event replay. What does not trigger a webhook retry? HTTP `4xx` responses (except `429`), DNS failures, and invalid hostnames are treated as permanent failures and will not be retried. Check your delivery logs in the [Linq dashboard](https://dashboard.linqapp.com/api-tooling/logs) to diagnose issues. Why am I not receiving webhooks? Common causes: 1. **Endpoint not reachable** — Ensure your URL is publicly accessible over HTTPS 2. **Timeout** — Your endpoint must respond with a `2xx` status within 10 seconds 3. **Wrong events** — Verify you subscribed to the correct event types 4. **Firewall rules** — Allow inbound requests from Linq’s IP ranges See [Debugging](/guides/platform/debugging/index.md) for webhook troubleshooting tools. ## Environments How can team members access the dashboard? You can either have your account set up with a shared team email, or invite users from the [Users section](https://dashboard.linqapp.com/users) of the dashboard. What's the best way to separate lines for testing, staging, and production? Although you can control which lines are used for what purposes on your end, to prevent accidental deployment of test features into production, we recommend segmenting lines into separate environments entirely. To have this done, please reach out to your Linq representative and request your desired setup. We will separate your numbers for you. Each environment will have its own dashboard and API key, which you can access via the admin login(s) we provide for each environment. ## Chat Health & Phone Reputation Does Linq read message content or store PII to compute chat health or phone reputation? No. Health checks run on anonymous, aggregate signals only — message volume, sends vs. receives, response cadence, and similar metadata. Linq does not collect or store message content, recipient identities, or other PII to compute chat `health_status` or phone `reputation`. See [Chat Health](/guides/chats/chat-health/index.md) and [Phone Reputation](/guides/phone-numbers/phone-reputation/index.md) for what the signals indicate and how to act on them. ## Limitations What are the rate limits? The API enforces a per-phone-pair limit of 30 messages in any 60-second window between the same sender and recipient, plus a 100 messages/day cap on sandbox accounts. There is no hard daily cap on production accounts — we recommend staying under 7,000 inbound + outbound messages per line per day for best performance. Exceeding a limit returns HTTP `429` with error code [`1007`](/error/codes/1xxx/1007/index.md) and a `Retry-After` header. See [Rate Limits](/guides/platform/rate-limits/index.md) for details. What is Linq doing to help prevent my numbers from being flagged? As responsible stewards of the iMessage ecosystem, we’re committed to providing you with the necessary tools and alerts to not only ensure your lines stay operational, but to help you deliver your end users the best possible experience. We’ve recently released a new beta feature called [Chat Health](/guides/chats/chat-health/index.md) that surfaces risky chats early, letting you make adjustments proactively to prevent flagged lines. What counts as a reply from a message reciprocity standpoint? Both messages back and tapback reactions count as responses and boost your reciprocity score. How much engagement should I aim for to keep lines in good standing? You should aim to elicit 3+ responses from a new user as soon as possible in order to keep conversations healthy. You should also aim for a message reciprocity ratio of 1:2 (inbound:outbound) for optimal results. How can I reduce the incidence of flagging? The following are best practices to reduce flagging risk: **Inbound flow:** Have users message your number first. Recipient-initiated chats can’t be reported by the recipient, which reduces the risk of your line being flagged. **Load balance:** Have users onboard evenly across multiple lines and over-provision by 10–20% to account for peak times and virality. We recommend onboarding users via deeplink from your webpage so you can rotate numbers with each signup. **Ramp up usage gradually:** Onboard users evenly and gradually to avoid large volume spikes. **Ensure your AI isn’t aggressive:** AI that aggressively follows up (especially unsolicited) can cause users to block your number, which increases the chance of your line being flagged. **Have clear opt-out detection:** Honor opt-out language from your users (e.g. stop, unsubscribe, or negative sentiment generally) and watch for the [`health_status.status: OPTED_OUT`](/guides/chats/chat-health#opted-out/index.md) signal on chats. **Monitor line and chat health:** Subscribe to the [`phone_number.status_updated`](/guides/webhooks/events#phone-number-events/index.md) webhook to react to line-level transitions, and use [Chat Health](/guides/chats/chat-health/index.md) to fix at-risk conversations before they pull the whole line down. **Use multiple lines:** We recommend sharding across multiple numbers so that if one were to become unavailable, your service will still remain operational. You can also put multiple fallback Linq Blue numbers on `.vcf` files you share with users at onboarding to ensure they can continue to receive communications in their existing threads should the primary number become flagged. If my line is flagged, will I still receive inbound messages? It is possible that you might receive inbound messages for a short while, but it’s expected that all activity to and from the line will fail at some point. The expected behavior is that iMessages will fail first, followed by RCS/SMS. If my line gets flagged, how long will it be down? Flagged lines typically see downtime of around 24 hours, but may be longer depending on severity. You will be able to check the recovery status in the [API toolkit](https://dashboard.linqapp.com/api-tooling/phone-numbers) section of the dashboard, and will receive a `phone_number.status_updated` webhook event notifying you when the number is active again if you’ve subscribed to it. What typically causes individual lines to become disabled? “Flagged” is one of the most common reasons why your line may become non-operational. A line typically transitions to `FLAGGED` when recipients report it heavily, when message reciprocity is low, or when sending behavior looks automated — sudden volume spikes are a common trigger. You can monitor the status of your lines in the [API toolkit](https://dashboard.linqapp.com/api-tooling/phone-numbers) in the dashboard. We also encourage you to subscribe to the `phone_number.status_updated` webhook to detect flagged lines programmatically. You can also enable Slack notifications to be alerted in your linked partner channel whenever a line’s status changes — see below. How do I know when a phone number's status changes? Subscribe to the [`phone_number.status_updated`](/guides/webhooks/events#phone-number-events/index.md) webhook event to receive a real-time payload whenever a number transitions between `ACTIVE` and `FLAGGED`. You can also enable **Flagged-number Slack notifications** in the [API Tooling](https://dashboard.linqapp.com/api-tooling/phone-numbers) settings to get a message in your linked partner channel whenever a number’s status changes. See [Phone Numbers](/guides/phone-numbers/index.md) for details. One of my lines is disabled — how do I migrate users? If users need to be migrated to a new line, we recommend setting up a dedicated broadcast number to announce issues/outages, and to direct users to working lines. The best tool for this would be an A2P registered SMS line reserved for this purpose. Email and push notifications (if you have an app) are also recommended. It’s preferred that migrations to different numbers take place by prompting users to inbound to the new number rather than outbounding to users from that new number, unless you know for certain that the user is actively engaging with your service. How can I mitigate line-specific downtime risk? We highly recommend using backup numbers to ensure service is available should any one of your lines become inoperable. To implement this, add one or more backup numbers to a `.vcf` file along with your primary number and share it with users upon onboarding (ideally via web download at signup) for them to add to their contact list. If your service were to fall back to any of the backup numbers, users would still see messages come through in their existing thread, preserving continuity. What should I expect for systemic downtime? Scheduled maintenance will be communicated to you in advance. In the event of a platform outage, you can monitor live as well as subscribe to updates via our [status page](https://status.linqapp.com/). How do volume limits translate to concurrent users? The approach to determining the number of concurrent users each of your lines can support is nuanced, given factors including how your AI agent behaves (does it send many or few messages) and how your users behave (do they converse with your AI, or simply make requests of it). We recommend utilizing a pilot line, onboarding users onto that line, and calculating the average number of inbound/outbound messages per user. You can then use the resulting amount to determine how many users can onboard onto your line before you hit the 7,000 messages/line/day guideline. How many messages can I send per line? We recommend limiting message volume to a combined total of up to 7,000 inbound and outbound messages per day per line. There are no hard daily caps on production accounts — this recommendation is meant to optimize line performance. See [Rate Limits](/guides/platform/rate-limits/index.md) for details. ## MCP Server What is the Linq MCP server? The Linq MCP server is a [Model Context Protocol](https://modelcontextprotocol.io/) server that lets AI tools like Claude Desktop, Cursor, Windsurf, and Claude Code interact with the Linq API directly. How do I install the MCP server? **npx (no install required):** Terminal window ``` npx -y @linqapp/sdk-mcp@latest ``` **Claude Code:** Terminal window ``` claude mcp add linq-api -- npx -y @linqapp/sdk-mcp@latest ``` **Claude Desktop / Cursor / Windsurf (`mcpServers.json`):** ``` { "mcpServers": { "linq-api": { "command": "npx", "args": ["-y", "@linqapp/sdk-mcp@latest"], "env": { "LINQ_API_V3_API_KEY": "your-api-key" } } } } ``` What can the MCP server do? The MCP server exposes two tools to LLMs: 1. **Code execution** — Run TypeScript code against the Linq SDK in a sandboxed environment 2. **Documentation search** — Query Linq API documentation in Markdown format, optimized for LLM consumption This means an AI assistant can search the docs, construct API calls, and execute them — all within a single conversation. ## LLM-Friendly Docs What are LLM-friendly docs? Linq publishes machine-readable versions of the API documentation, optimized for use by large language models. These follow the [`llms.txt` standard](https://llmstxt.org/) and provide structured Markdown that LLMs can consume directly. How do I access the LLM-friendly docs? The following endpoints are available on the docs site: | Endpoint | Description | | ---------------- | ------------------------------------------------------------------------------ | | `/llms.txt` | Index of available documentation pages in a concise format | | `/llms-full.txt` | Complete API documentation in a single file, optimized for LLM context windows | These files are automatically kept in sync with the latest API documentation. When should I use LLM-friendly docs vs. the MCP server? - Use **`llms.txt` / `llms-full.txt`** when you want to feed Linq API docs into a custom LLM pipeline, RAG system, or prompt context - Use the **MCP server** when you want an AI assistant (Claude, Cursor, etc.) to interactively search docs and execute API calls on your behalf Can I use these with my own AI agents? Yes. The `llms-full.txt` file is ideal for embedding Linq API knowledge into your own AI agents. Fetch it at build time or runtime and include it in your system prompt or retrieval pipeline. Combined with the Linq API itself, this lets you build AI agents that understand and use the Linq API without manual documentation parsing. --- # Migrating from V2 to V3 URL: https://docs.linqapp.com/guides/resources/migration-v2-to-v3/ V3 is a full redesign — not a patch release. The core change is a shift from **synchronous** responses to an **asynchronous** model: the API accepts your request immediately and returns a `trace_id`, while webhooks tell you what happened. Along the way, the message structure, authentication header, endpoint paths, and error shape all changed. This guide walks through every breaking change and new capability. ## Why migrate? | You want to… | V2 | V3 | | ---------------------------------------- | ---------- | ----------------------------------- | | Know when messages actually fail | ❌ | ✅ `message.failed` webhook | | Know exactly when messages are delivered | ❌ | ✅ `message.delivered` webhook | | Debug issues quickly | ❌ | ✅ `trace_id` everywhere | | Choose message protocols | ❌ | ✅ `preferred_service: iMessage` | | Send confetti/effects | ❌ | ✅ 15 effects (11 screen + 4 bubble) | | Use custom emoji reactions | ❌ | ✅ Any emoji | | Send Office docs, ZIP files | ❌ | ✅ 18 document types | | Know when group names/icons change | ❌ | ✅ Group metadata webhooks | | Reply to specific messages | ⚠️ Limited | ✅ Full threading | | Send voice memos | ❌ | ✅ Voice memo API | --- ## Quick migration checklist - [ ] Replace `X-LINQ-INTEGRATION-TOKEN` header with `Authorization: Bearer <token>` - [ ] Update all base paths from `/api/partner/v2/` to `/api/partner/v3/` - [ ] Map existing V2 chat IDs to their V3 UUIDs via the [migration endpoint](#migrating-existing-chats) - [ ] Convert message body: `text`/`attachments` → `parts` array - [ ] Parse `success` boolean and log the `trace_id` on every response - [ ] Update webhook handlers for the new payload structure; use `event_id` for deduplication - [ ] Switch to the presigned upload flow for attachments - [ ] Register webhook subscriptions programmatically (see [Webhook Subscriptions](/guides/webhooks/subscriptions/index.md)) --- ## Authentication The token value stays the same — only the header name changes. | | V2 | V3 | | ------ | ----------------------------------- | ------------------------------- | | Header | `X-LINQ-INTEGRATION-TOKEN: <token>` | `Authorization: Bearer <token>` | Terminal window ``` # V2 curl -H "X-LINQ-INTEGRATION-TOKEN: $LINQ_API_KEY" ... # V3 curl -H "Authorization: Bearer $LINQ_API_KEY" ... ``` See [Authentication](/getting-started/authentication/index.md) for full details. --- ## Migrating existing chats Chat IDs changed between versions: V2 chat IDs are integers (e.g. `12345`), while V3 chat IDs are UUIDs (e.g. `550e8400-e29b-41d4-a716-446655440000`). To keep an in-flight V2 conversation going under V3, look up its V3 chat ID with the migration endpoint: ``` GET /api/partner/v2/chats/{v2ChatID}/migrate_to_v3 ``` This is a **V2 endpoint**, so it uses the V2 authentication header (`X-LINQ-INTEGRATION-TOKEN`), not the V3 `Authorization: Bearer` header. It takes no request body. **Path parameters:** | Param | Type | Required | Notes | | ---------- | ------- | -------- | -------------- | | `v2ChatID` | integer | ✅ | The V2 chat ID | Terminal window ``` curl https://api.linqapp.com/api/partner/v2/chats/12345/migrate_to_v3 \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" ``` The call is **idempotent** — migrating an already-migrated chat returns the same response as the first migration. Once you have the `v3_chat_id`, use it as the `chatId` in every V3 endpoint. ### Responses **`200 OK`** — the chat was migrated (or was already migrated). Both cases return the same shape: ``` { "data": { "v3_chat_id": "550e8400-e29b-41d4-a716-446655440000", "v2_chat_id": 12345, "migrated": true } } ``` **`422 Unprocessable Entity`** — the chat could not be migrated. `v3_chat_id` is `null` and `migrated` is `false`: ``` { "data": { "v3_chat_id": null, "v2_chat_id": 12345, "migrated": false } } ``` **`404 Not Found`** — the chat doesn’t exist, or belongs to another organization. This returns the V2 error shape: ``` { "errors": [ { "status": 404, "code": "not_found", "title": "Not found", "detail": "Chat not found" } ] } ``` --- ## Sending messages ### Endpoint path The flat `/v2/chat_messages` endpoint is gone. V3 nests messages under the chat resource: | Operation | V2 | V3 | | ------------ | ------------------------ | ---------------------------------- | | Send message | `POST /v2/chat_messages` | `POST /v3/chats/{chatId}/messages` | ### Message structure V2 used top-level `text` and `attachments` fields. V3 uses a unified `parts` array where each part has an explicit `type`. **V2 request body:** ``` { "chat_id": "chat-uuid", "text": "Hello, world!", "attachments": [{ "url": "https://example.com/image.jpg" }] } ``` **V3 request body:** ``` { "message": { "parts": [ { "type": "text", "value": "Hello, world!" }, { "type": "media", "attachment_id": "att-uuid" } ] } } ``` The `chat_id` moves from the body to the URL path. Message content is wrapped in a `message` object. Attachments must be pre-uploaded and referenced by `attachment_id` — see [Attachments](#attachments) below. See [Sending Messages](/guides/messaging/sending-messages/index.md) and the [Send Message](/api/resources/chats/subresources/messages/methods/send/index.md) API reference for the full schema. --- ## Response format V3 success responses return the resource directly — there is no `data` wrapper or `success` field on success. **V2 response:** ``` { "data": { "chat_message": { "id": "msg-uuid", "text": "Hello" } }, "status": 200 } ``` **V3 success response (send message):** ``` { "chat_id": "chat-uuid", "message": { "id": "msg-uuid", "delivery_status": "pending", "service": null, "parts": [...], } } ``` **V3 error response:** ``` { "success": false, "error": { "status": 400, "code": 1005, "message": "invalid chatId format: must be a valid UUID", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1005/" }, "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" } ``` Key changes: - `success: false` and `trace_id` appear in **error responses only** — not in the success body - `trace_id` on successful requests is returned in the `X-Trace-ID` **response header**; log it for every request - `service` is `null` on the initial response; it is confirmed via the `message.delivered` webhook for iMessage and RCS — SMS does not produce a delivery receipt --- ## Async processing and trace IDs V2 was synchronous — you waited for a result. V3 is asynchronous: 1. The API accepts your request and returns a `message_id` in the body plus a `trace_id` in the `X-Trace-ID` response header immediately. 2. Processing happens in the background. 3. Webhooks notify you of the outcome: `message.sent` → `message.delivered` or `message.failed`. This means **silent failures are gone**. Any failure will produce a `message.failed` webhook with error details. Always log the `trace_id` — it’s how you correlate an API request with its webhook events and is the first thing support will ask for. See [Webhooks](/guides/webhooks/index.md) and [Debugging](/guides/platform/debugging/index.md) for more. --- ## Error handling V3 uses structured error responses with numeric codes. **V2 error:** ``` { "error": "Invalid chat_id", "status": 400 } ``` **V3 error:** ``` { "success": false, "error": { "status": 400, "code": 1005, "message": "invalid chatId format: must be a valid UUID", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1005/" }, "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" } ``` Update your error handling to read `error.code` (numeric) rather than parsing the `error` string. See the [Errors](/error/index.md) guide for the full code reference. --- ## Webhooks V3 webhook payloads have a new structure with versioning and deduplication support. **V2 payload:** ``` { "event": "message.sent", "data": { "chat_message": { "id": "msg-uuid" } } } ``` **V3 payload:** ``` { "api_version": "v3", "event_type": "message.sent", "event_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2025-11-23T17:30:00Z", "trace_id": "abc123def456", "data": { "message_id": "msg-uuid", "service": "iMessage" } } ``` Key differences: - `event_type` replaces `event` - `event_id` is a stable UUID per event — use it for deduplication; webhooks are delivered at-least-once with up to 10 retries over \~25 minutes - `trace_id` links the event back to the originating API request - `data` shape changed — update your payload destructuring - Register and manage subscriptions via the API — see [Webhook Subscriptions](/guides/webhooks/subscriptions/index.md) **New events in V3:** | Event | Description | | ------------------------------- | ----------------------------------- | | `message.delivered` | Explicit delivery confirmation | | `message.failed` | Delivery failure with error details | | `chat.group_name_updated` | Group name was changed | | `chat.group_icon_updated` | Group icon was changed | | `chat.group_name_update_failed` | Group name update failed | | `chat.group_icon_update_failed` | Group icon update failed | See the [Webhook Events](/guides/webhooks/events/index.md) guide for the full event reference. --- ## Attachments V2 accepted a direct URL in the message body. V3 uses a two-step presigned upload flow. **Step 1 — Request an upload URL:** Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/attachments \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "document.pdf", "content_type": "application/pdf", "size_bytes": 1024000 }' ``` **Step 2 — Upload to the presigned S3 URL:** Terminal window ``` curl -X PUT "https://uploads.linqapp.com/..." \ -H "Content-Type: application/pdf" \ --data-binary @document.pdf ``` **Step 3 — Reference the `attachment_id` in your message:** ``` { "message": { "parts": [{ "type": "media", "attachment_id": "att-uuid" }] } } ``` V3 also expands supported document types from 2 to 18, including Word, Excel, PowerPoint, Pages, Numbers, Keynote, PDF, TXT, RTF, CSV, HTML, ePub, ZIP, and `.ics` files. See the [Attachments](/guides/messaging/attachments/index.md) guide for the full list and size limits. --- ## Group chats V3 simplifies group creation by combining it with the initial message, and adds explicit participant management endpoints. **V2 creation:** ``` POST /api/partner/v2/chats { "phone_numbers": ["+12025550001", "+12025550002"], "display_name": "My Group" } ``` **V3 creation:** ``` POST /api/partner/v3/chats { "from": "+12025550100", "to": ["+12025550001", "+12025550002"], "message": { "parts": [{ "type": "text", "value": "Welcome to the group!" }] } } ``` **New participant management endpoints:** | Operation | V3 Path | | --------------------- | ---------------------------------------- | | Add participants | `POST /v3/chats/{chatId}/participants` | | Remove participants | `DELETE /v3/chats/{chatId}/participants` | | Leave group chat | `POST /v3/chats/{chatId}/leave` | | Update group metadata | `PUT /v3/chats/{chatId}` | See the [Group Chats](/guides/chats/group-chats/index.md) guide for details. --- ## Not supported yet The following V2 features are not yet available in V3. ### Contacts API V2 included a full contacts management API (`GET/POST/PUT/PATCH/DELETE /v2/contacts`). V3 does not currently have a contacts API. If your integration relies on creating or managing contacts via V2, you will need to continue using V2 for this or handle contact management outside of the Linq API for now. ### Phone number forwarding V2 exposed `PUT /v2/phone_numbers/{id}` to update a number’s forwarding configuration. This is not yet available in V3. --- ## Full endpoint reference **V2 → V3 path changes:** | Operation | V2 | V3 | | --------------------- | ------------------------------------------------ | -------------------------------------------- | | List chats | `GET /v2/chats` | `GET /v3/chats` | | Create chat | `POST /v2/chats` | `POST /v3/chats` | | Get chat | `GET /v2/chats/{id}` | `GET /v3/chats/{chatId}` | | Mark as read | `PUT /v2/chats/{id}/mark_as_read` | `POST /v3/chats/{chatId}/read` | | Share contact card | `POST /v2/chats/{id}/share_contact` | `POST /v3/chats/{chatId}/share_contact_card` | | Start typing | `POST /v2/chats/{id}/start_typing` | `POST /v3/chats/{chatId}/typing` | | Stop typing | `DELETE /v2/chats/{id}/stop_typing` | `DELETE /v3/chats/{chatId}/typing` | | List messages | `GET /v2/chats/{id}/chat_messages` | `GET /v3/chats/{chatId}/messages` | | Send message | `POST /v2/chats/{id}/chat_messages` | `POST /v3/chats/{chatId}/messages` | | Get message | `GET /v2/chats/{id}/chat_messages/{msgId}` | `GET /v3/messages/{messageId}` | | Edit message | `POST /v2/chats/{id}/chat_messages/{msgId}/edit` | `PATCH /v3/messages/{messageId}` | | Delete message | `DELETE /v2/chats/{id}/chat_messages/{msgId}` | `DELETE /v3/messages/{messageId}` | | Add/remove reaction | `POST /v2/chat_messages/{id}/reactions` | `POST /v3/messages/{messageId}/reactions` | | List phone numbers | `GET /v2/phone_numbers` | `GET /v3/phone_numbers` | | Webhook subscriptions | `/v2/webhook_subscriptions` | `/v3/webhook-subscriptions` | | iMessage availability | `POST /v2/i_message_availability/check` | `POST /v3/capability/check_imessage` | To resolve an existing V2 chat’s integer ID to its V3 UUID, call `GET /v2/chats/{v2ChatID}/migrate_to_v3` — see [Migrating existing chats](#migrating-existing-chats). **New in V3 (no V2 equivalent):** | Operation | V3 Path | | ----------------------- | ---------------------------------------- | | Update chat metadata | `PUT /v3/chats/{chatId}` | | Add participants | `POST /v3/chats/{chatId}/participants` | | Remove participants | `DELETE /v3/chats/{chatId}/participants` | | Leave group chat | `POST /v3/chats/{chatId}/leave` | | Send voice memo | `POST /v3/chats/{chatId}/voicememo` | | Get thread | `GET /v3/messages/{messageId}/thread` | | Upload attachment | `POST /v3/attachments` | | Get attachment metadata | `GET /v3/attachments/{attachmentId}` | --- ## New V3 features These capabilities have no V2 equivalent. **[Protocol selection](/guides/messaging/protocol-selection/index.md)** — Use `preferred_service` on any message to target a specific channel (`iMessage`, `RCS`, or `SMS`). Useful for iMessage-only features or compliance requirements. **[Message effects](/guides/messaging/message-effects/index.md)** — Attach iMessage screen effects (confetti, fireworks, lasers, sparkles) or bubble effects (slam, loud, gentle, invisible ink) via the `effect` object. **[Text decorations](/guides/messaging/sending-messages#text-decorations/index.md)** — Apply bold, italic, strikethrough, underline, and animated styles to character ranges within a text part. iMessage only. **[Threaded replies](/guides/messaging/sending-messages#replying-to-messages/index.md)** — Reply to a specific message using `reply_to: { message_id, part_index }`. **[Custom reactions](/guides/messaging/reactions/index.md)** — V2 supported the six standard iMessage tapbacks. V3 adds custom emoji reactions on top of those. **[Voice memos](/guides/messaging/voice-memos/index.md)** — Send audio messages as iMessage voice memo bubbles via `POST /v3/chats/{chatId}/voicememo`. **[Participant management](/guides/chats/group-chats/index.md)** — Add and remove participants from existing group chats, and leave a group, via dedicated endpoints. **[Rich link previews](/guides/messaging/rich-link-previews/index.md)** — Send a `link` part to render a rich preview card on iMessage and RCS. **[Presigned attachment uploads](/guides/messaging/attachments/index.md)** — Upload files via a presigned S3 URL and reference them by `attachment_id`. Supports 18 document types vs. 2 in V2. --- # Webhook Events URL: https://docs.linqapp.com/guides/webhooks/events/ Every webhook delivered by Linq shares a common envelope with a `data` field specific to the event type. This page lists the event types you can subscribe to and shows a representative payload per category. Each example is sourced directly from the OpenAPI spec — pick a version tab on any example and every other example on the page switches to that version too. For setup, signature verification, and delivery guarantees see [Webhooks](/guides/webhooks/index.md); for subscription management see [Webhook Subscriptions](/guides/webhooks/subscriptions/index.md). > **Authoritative list of event types** (rendered from the OpenAPI spec): `message.sent`, `message.received`, `message.read`, `message.delivered`, `message.failed`, `message.edited`, `reaction.added`, `reaction.removed`, `participant.added`, `participant.removed`, `chat.created`, `chat.group_name_updated`, `chat.group_icon_updated`, `chat.group_name_update_failed`, `chat.group_icon_update_failed`, `chat.typing_indicator.started`, `chat.typing_indicator.stopped`, `phone_number.status_updated`, `call.initiated`, `call.ringing`, `call.answered`, `call.ended`, `call.failed`, `call.declined`, `call.no_answer`, `location.sharing.started`, `location.sharing.stopped`. > > Categories below describe the most common events. If a type appears in this canonical list but not in a category section, treat the canonical list as source of truth and refer to the [API Reference: Webhook Events](/api/resources/webhooks/index.md) for the payload schema. ## Webhook envelope All webhook payloads share a common envelope: ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "message.received", "event_id": "2915e81c-5068-4796-ace2-21d2c94ad298", "created_at": "2026-02-05T19:31:13.736Z", "trace_id": "8af9171a45022df2eb74ba4e4c83be0f", "partner_id": "your-partner-id", "data": { ... } } ``` | Field | Description | | ----------------- | ---------------------------------------------- | | `api_version` | API version (`v3`) | | `webhook_version` | Payload version (`2025-01-01` or `2026-02-03`) | | `event_type` | Event type string | | `event_id` | Unique event ID (use for deduplication) | | `created_at` | When the event was created | | `trace_id` | Trace ID for debugging | | `partner_id` | Your partner identifier | | `data` | Event-specific payload | ## Message events | Event | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `message.sent` | Message successfully sent from your phone number | | `message.received` | Incoming message received on your phone number | | `message.delivered` | Message delivered to recipient’s device | | `message.read` | Message read by recipient | | `message.failed` | Message delivery failed | | `message.edited` | Message was edited (2026-02-03 version only) — see [Editing messages](/guides/messaging/sending-messages#editing-messages/index.md) | **Lifecycle timestamps** indicate the event stage: - `sent_at` — set on `message.sent` and `message.received` - `delivered_at` — set on `message.delivered` - `read_at` — set on `message.read` `message.edited` is only delivered to `2026-02-03` subscriptions. In `2026-02-03` payloads, the `chat` sub-object includes a `health_status` field — see [Chat Health](/guides/chats/chat-health/index.md). > **Note:** SMS and MMS sends **do not produce `message.delivered` or `message.read` webhooks** — neither receipt type exists on those protocols. You’ll still get `message.sent` (accepted for delivery) and `message.failed` (hard failure), but treat the absence of a `delivered` event on an SMS fallback as normal, not a bug. See [Protocol capabilities](/guides/messaging/protocol-selection#protocol-capabilities/index.md) for the full matrix. ### `message.sent` Outbound message confirmed as sent from your phone number. `delivered_at` and `read_at` are still null. - [2026-02-03](#tab-panel-141) - [2025-01-01](#tab-panel-142) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "message.sent", "event_id": "e20feb41-7f67-43f0-89c8-a985cff3b568", "created_at": "2026-02-05T19:52:18.101373886Z", "trace_id": "2eff5df5c6f688733c007523c4d61cd9", "partner_id": "your-partner-id", "data": { "chat": { "id": "0c961e93-e7bf-4db2-bf7b-ea06826bcab4", "is_group": false, "owner_handle": { "handle": "+12025551234", "id": "8d79532a-f529-4244-a5cf-d443de051434", "is_me": true, "joined_at": "2026-01-21T21:59:45.191571Z", "left_at": null, "service": "iMessage", "status": "active" }, "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#healthy", "updated_at": "2026-02-05T19:52:17.000Z" } }, "id": "347d62c2-2170-4754-8d30-c76d0c727d96", "idempotency_key": null, "direction": "outbound", "sender_handle": { "handle": "+12025551234", "id": "8d79532a-f529-4244-a5cf-d443de051434", "is_me": true, "joined_at": "2026-01-21T21:59:45.191571Z", "left_at": null, "service": "iMessage", "status": "active" }, "parts": [ { "type": "text", "value": "Hello from Linq!" }, { "filename": "photo.jpg", "id": "f13dda7d-ecac-49eb-b3fe-16fe286abf19", "mime_type": "image/jpeg", "size_bytes": 245678, "type": "media", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/photo.jpg?signature=..." } ], "effect": null, "sent_at": "2026-02-05T19:52:17.219Z", "delivered_at": null, "read_at": null, "service": "iMessage", "preferred_service": null } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "message.sent", "event_id": "e20feb41-7f67-43f0-89c8-a985cff3b568", "created_at": "2026-02-05T19:52:18.101373886Z", "trace_id": "2eff5df5c6f688733c007523c4d61cd9", "partner_id": "your-partner-id", "data": { "chat_id": "0c961e93-e7bf-4db2-bf7b-ea06826bcab4", "from": "+12025551234", "from_handle": { "handle": "+12025551234", "id": "8d79532a-f529-4244-a5cf-d443de051434", "is_me": true, "joined_at": "2026-01-21T21:59:45.191571Z", "left_at": null, "service": "iMessage", "status": "active" }, "idempotency_key": null, "is_from_me": true, "is_group": false, "message": { "created_at": "2026-02-05T19:52:17.041183Z", "delivered_at": null, "id": "347d62c2-2170-4754-8d30-c76d0c727d96", "is_delivered": false, "is_read": false, "parts": [ { "type": "text", "value": "Hello from Linq!" }, { "filename": "photo.gif", "id": "f13dda7d-ecac-49eb-b3fe-16fe286abf19", "mime_type": "image/gif", "size_bytes": 2776819, "type": "media", "url": "https://cdn.linqapp.com/attachments/example/photo.gif" } ], "read_at": null, "sent_at": "2026-02-05T19:52:17.219Z", "updated_at": "2026-02-05T19:52:18.084038Z" }, "preferred_service": null, "received_at": null, "recipient_handle": null, "recipient_phone": null, "service": "iMessage" } } ``` ### `message.received` Inbound message from a participant. Contains the full message content and any attachments. - [2026-02-03](#tab-panel-143) - [2025-01-01](#tab-panel-144) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "message.received", "event_id": "2915e81c-5068-4796-ace2-21d2c94ad298", "created_at": "2026-02-05T19:31:13.736444093Z", "trace_id": "8af9171a45022df2eb74ba4e4c83be0f", "partner_id": "your-partner-id", "data": { "chat": { "id": "8f392755-6865-4b18-880a-227f9d8b458f", "is_group": false, "owner_handle": { "handle": "+12025551234", "id": "6d6c617f-187a-4dcd-a0d5-988347a8c092", "is_me": true, "joined_at": "2026-01-04T05:48:51.321469Z", "left_at": null, "service": "iMessage", "status": "active" }, "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#healthy", "updated_at": "2026-02-05T19:31:12.000Z" } }, "id": "89e3566e-1d13-49e5-a8ee-48490d5bfeb7", "direction": "inbound", "sender_handle": { "handle": "+12025559876", "id": "e604375a-5913-483a-8278-c631e8f0ffda", "is_me": false, "joined_at": "2026-01-04T05:48:51.321469Z", "left_at": null, "service": "iMessage", "status": "active" }, "parts": [ { "type": "text", "value": "Hello!" } ], "effect": null, "reply_to": null, "sent_at": "2026-02-05T19:31:13.074Z", "delivered_at": null, "read_at": null, "service": "iMessage" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "message.received", "event_id": "2915e81c-5068-4796-ace2-21d2c94ad298", "created_at": "2026-02-05T19:31:13.736444093Z", "trace_id": "8af9171a45022df2eb74ba4e4c83be0f", "partner_id": "your-partner-id", "data": { "chat_id": "8f392755-6865-4b18-880a-227f9d8b458f", "from": "+12025559876", "from_handle": { "handle": "+12025559876", "id": "e604375a-5913-483a-8278-c631e8f0ffda", "is_me": false, "joined_at": "2026-01-04T05:48:51.321469Z", "left_at": null, "service": "iMessage", "status": "active" }, "idempotency_key": null, "is_from_me": false, "is_group": false, "message": { "created_at": "2026-02-05T19:31:12.892Z", "delivered_at": null, "id": "89e3566e-1d13-49e5-a8ee-48490d5bfeb7", "is_delivered": false, "is_read": false, "parts": [ { "type": "text", "value": "Hello!" } ], "read_at": null, "sent_at": "2026-02-05T19:31:13.074Z", "updated_at": "2026-02-05T19:31:13.712Z" }, "preferred_service": null, "received_at": "2026-02-05T19:31:13.074Z", "recipient_handle": { "handle": "+12025551234", "id": "6d6c617f-187a-4dcd-a0d5-988347a8c092", "is_me": true, "joined_at": "2026-01-04T05:48:51.321469Z", "left_at": null, "service": "iMessage", "status": "active" }, "recipient_phone": null, "service": "iMessage" } } ``` ### `message.delivered` Outbound message reached the recipient’s device. Fires on iMessage and RCS — not on SMS/MMS, which have no delivery receipt mechanism. - [2026-02-03](#tab-panel-145) - [2025-01-01](#tab-panel-146) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "message.delivered", "event_id": "67c4ad39-e9b0-47f6-82f8-64bdd8ceafa6", "created_at": "2026-02-05T19:52:22.593689073Z", "trace_id": "abde7f6248fba00f97e8c7dc4782d7e0", "partner_id": "your-partner-id", "data": { "chat": { "id": "0c961e93-e7bf-4db2-bf7b-ea06826bcab4", "is_group": false, "owner_handle": { "handle": "+12025551234", "id": "8d79532a-f529-4244-a5cf-d443de051434", "is_me": true, "joined_at": "2026-01-21T21:59:45.191571Z", "left_at": null, "service": "iMessage", "status": "active" }, "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#healthy", "updated_at": "2026-02-05T19:52:17.000Z" } }, "id": "347d62c2-2170-4754-8d30-c76d0c727d96", "idempotency_key": null, "direction": "outbound", "sender_handle": { "handle": "+12025551234", "id": "8d79532a-f529-4244-a5cf-d443de051434", "is_me": true, "joined_at": "2026-01-21T21:59:45.191571Z", "left_at": null, "service": "iMessage", "status": "active" }, "parts": [ { "type": "text", "value": "Hello from Linq!" }, { "filename": "photo.gif", "id": "f13dda7d-ecac-49eb-b3fe-16fe286abf19", "mime_type": "image/gif", "size_bytes": 2776819, "type": "media", "url": "https://cdn.linqapp.com/attachments/f13dda7d/photo.gif?signature=..." } ], "effect": null, "sent_at": "2026-02-05T19:52:17.219Z", "delivered_at": "2026-02-05T19:52:22.291Z", "read_at": null, "service": "iMessage", "preferred_service": null } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "message.delivered", "event_id": "67c4ad39-e9b0-47f6-82f8-64bdd8ceafa6", "created_at": "2026-02-05T19:52:22.593689073Z", "trace_id": "abde7f6248fba00f97e8c7dc4782d7e0", "partner_id": "your-partner-id", "data": { "chat_id": "0c961e93-e7bf-4db2-bf7b-ea06826bcab4", "from": "+12025551234", "from_handle": { "handle": "+12025551234", "id": "8d79532a-f529-4244-a5cf-d443de051434", "is_me": true, "joined_at": "2026-01-21T21:59:45.191571Z", "left_at": null, "service": "iMessage", "status": "active" }, "idempotency_key": null, "is_from_me": true, "is_group": false, "message": { "created_at": "2026-02-05T19:52:17.041183Z", "delivered_at": "2026-02-05T19:52:22.291Z", "id": "347d62c2-2170-4754-8d30-c76d0c727d96", "is_delivered": true, "is_read": false, "parts": [ { "type": "text", "value": "Hello from Linq!" }, { "filename": "photo.gif", "id": "f13dda7d-ecac-49eb-b3fe-16fe286abf19", "mime_type": "image/gif", "size_bytes": 2776819, "type": "media", "url": "https://cdn.linqapp.com/attachments/example/photo.gif" } ], "read_at": null, "sent_at": "2026-02-05T19:52:17.219Z", "updated_at": "2026-02-05T19:52:22.571Z" }, "delivered_at": "2026-02-05T19:52:22.291Z", "message_id": "347d62c2-2170-4754-8d30-c76d0c727d96", "preferred_service": null, "received_at": null, "recipient_handle": null, "recipient_phone": null, "service": "iMessage" } } ``` ### `message.read` Outbound message was read by the recipient. Fires on iMessage and RCS (not SMS/MMS), and requires the recipient to have read receipts enabled on their device. - [2026-02-03](#tab-panel-147) - [2025-01-01](#tab-panel-148) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "message.read", "event_id": "8fd42065-b998-482a-93b3-da855f8dad17", "created_at": "2026-02-05T19:13:58.833366566Z", "trace_id": "cbb93c08fa1a3f3c4c2efc161d67f36d", "partner_id": "your-partner-id", "data": { "chat": { "id": "24e33345-e6cf-4f50-9d35-1d7fde8c9818", "is_group": false, "owner_handle": { "handle": "+12025551234", "id": "d31678e9-0442-48fd-b7ed-c898d245dd15", "is_me": true, "joined_at": "2026-01-18T03:38:41.442254Z", "left_at": null, "service": "iMessage", "status": "active" }, "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#healthy", "updated_at": "2026-02-05T19:13:57.000Z" } }, "id": "dc6d3f68-90df-48f0-a504-e65f239a383c", "idempotency_key": null, "direction": "outbound", "sender_handle": { "handle": "+12025551234", "id": "d31678e9-0442-48fd-b7ed-c898d245dd15", "is_me": true, "joined_at": "2026-01-18T03:38:41.442254Z", "left_at": null, "service": "iMessage", "status": "active" }, "parts": [ { "type": "text", "value": "Hello world!" } ], "effect": null, "sent_at": "2026-02-05T19:13:57.814Z", "delivered_at": "2026-02-05T19:13:57.948Z", "read_at": "2026-02-05T19:13:58.177Z", "service": "iMessage", "preferred_service": null } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "message.read", "event_id": "8fd42065-b998-482a-93b3-da855f8dad17", "created_at": "2026-02-05T19:13:58.833366566Z", "trace_id": "cbb93c08fa1a3f3c4c2efc161d67f36d", "partner_id": "your-partner-id", "data": { "chat_id": "24e33345-e6cf-4f50-9d35-1d7fde8c9818", "from": "+12025551234", "from_handle": { "handle": "+12025551234", "id": "d31678e9-0442-48fd-b7ed-c898d245dd15", "is_me": true, "joined_at": "2026-01-18T03:38:41.442254Z", "left_at": null, "service": "iMessage", "status": "active" }, "idempotency_key": null, "is_from_me": true, "is_group": false, "message": { "created_at": "2026-02-05T19:13:57.612Z", "delivered_at": "2026-02-05T19:13:57.948Z", "id": "dc6d3f68-90df-48f0-a504-e65f239a383c", "is_delivered": true, "is_read": true, "parts": [ { "type": "text", "value": "Hello world!" } ], "read_at": "2026-02-05T19:13:58.177Z", "sent_at": "2026-02-05T19:13:57.814Z", "updated_at": "2026-02-05T19:13:58.811Z" }, "message_id": "dc6d3f68-90df-48f0-a504-e65f239a383c", "preferred_service": null, "read_at": "2026-02-05T19:13:58.177Z", "received_at": null, "recipient_handle": null, "recipient_phone": null, "service": "iMessage" } } ``` ### `message.failed` Message delivery failed. `code` maps to an [error code](/error/index.md); common causes are request timeouts (`4001`), upstream processing errors, and service unavailability. - [2026-02-03](#tab-panel-149) - [2025-01-01](#tab-panel-150) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "message.failed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "550e8400-e29b-41d4-a716-446655440001", "code": 4001, "reason": "Delivery failed", "failed_at": "2025-11-23T17:35:00.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "message.failed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "550e8400-e29b-41d4-a716-446655440001", "code": 4001, "reason": "Delivery failed", "failed_at": "2025-11-23T17:35:00.000Z" } } ``` ### `message.edited` A text part of a previously sent message was edited. Only delivered to subscriptions on `webhook_version: "2026-02-03"` — see [Editing messages](/guides/messaging/sending-messages#editing-messages/index.md). - [2026-02-03](#tab-panel-140) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "message.edited", "event_id": "c3d4e5f6-a7b8-9012-cdef-345678901234", "created_at": "2026-03-05T02:12:46.501Z", "trace_id": "d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6", "partner_id": "your-partner-id", "data": { "chat": { "id": "8f392755-6865-4b18-880a-227f9d8b458f", "is_group": false, "owner_handle": { "handle": "+12025551234", "id": "6d6c617f-187a-4dcd-a0d5-988347a8c092", "is_me": true, "joined_at": "2026-01-04T05:48:51.321469Z", "left_at": null, "service": "iMessage", "status": "active" }, "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#healthy", "updated_at": "2026-03-05T02:12:45.000Z" } }, "id": "89e3566e-1d13-49e5-a8ee-48490d5bfeb7", "direction": "outbound", "sender_handle": { "handle": "+12025551234", "id": "6d6c617f-187a-4dcd-a0d5-988347a8c092", "is_me": true, "joined_at": "2026-01-04T05:48:51.321469Z", "left_at": null, "service": "iMessage", "status": "active" }, "part": { "index": 0, "text": "This is the edited message content" }, "edited_at": "2026-03-05T02:12:46.487Z" } } ``` ## Reaction events | Event | Description | | ------------------ | --------------------------------------------------------------------------------------------- | | `reaction.added` | Reaction (tapback) added to a message — see [Reactions](/guides/messaging/reactions/index.md) | | `reaction.removed` | Reaction removed from a message — see [Reactions](/guides/messaging/reactions/index.md) | Both events share the `ReactionEventBase` shape and are identical across versions. The `reaction_type` field indicates the kind of reaction: - **Tapback** — one of `love`, `like`, `dislike`, `laugh`, `emphasize`, `question` - **Custom emoji** — `reaction_type: "custom"`, emoji in `custom_emoji` - **Sticker** — `reaction_type: "sticker"`, details in a `sticker` object — see [Sticker attachments](/guides/messaging/reactions#sticker-attachments/index.md) ### `reaction.added` A standard tapback (`love`) added to a message part. - [2026-02-03](#tab-panel-151) - [2025-01-01](#tab-panel-152) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "reaction.added", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "550e8400-e29b-41d4-a716-446655440001", "part_index": 0, "reaction_type": "love", "custom_emoji": null, "is_from_me": false, "from": "+14155559876", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "service": "iMessage", "reacted_at": "2025-11-23T17:35:00.000Z", "sticker": null } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "reaction.added", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "550e8400-e29b-41d4-a716-446655440001", "part_index": 0, "reaction_type": "love", "custom_emoji": null, "is_from_me": false, "from": "+14155559876", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "service": "iMessage", "reacted_at": "2025-11-23T17:35:00.000Z", "sticker": null } } ``` ### `reaction.removed` Same shape as `reaction.added` — `reacted_at` is when the reaction was taken off. - [2026-02-03](#tab-panel-153) - [2025-01-01](#tab-panel-154) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "reaction.removed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "550e8400-e29b-41d4-a716-446655440001", "part_index": 0, "reaction_type": "love", "custom_emoji": null, "is_from_me": false, "from": "+14155559876", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "service": "iMessage", "reacted_at": "2025-11-23T17:35:00.000Z", "sticker": null } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "reaction.removed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "550e8400-e29b-41d4-a716-446655440001", "part_index": 0, "reaction_type": "love", "custom_emoji": null, "is_from_me": false, "from": "+14155559876", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "service": "iMessage", "reacted_at": "2025-11-23T17:35:00.000Z", "sticker": null } } ``` ## Chat events | Event | Description | | ------------------------------- | ---------------------------------------------------------------------------------------------- | | `chat.created` | New chat created — fires for DMs and group chats | | `chat.group_name_updated` | Group chat display name changed | | `chat.group_icon_updated` | Group chat icon changed | | `chat.group_name_update_failed` | Group name update failed | | `chat.group_icon_update_failed` | Group icon update failed | | `chat.typing_indicator.started` | Participant started typing — see [Typing Indicators](/guides/chats/typing-indicators/index.md) | | `chat.typing_indicator.stopped` | Participant stopped typing — see [Typing Indicators](/guides/chats/typing-indicators/index.md) | Chat-level events use a consistent shape across versions. ### `chat.created` The `data` block mirrors the `GET /v3/chats/{chatId}` response. In `2026-02-03` payloads, `health_status` is included — see [Chat Health](/guides/chats/chat-health/index.md). - [2026-02-03](#tab-panel-155) - [2025-01-01](#tab-panel-156) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "chat.created", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "display_name": "+14155551234, +14155559876", "service": "iMessage", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440010", "handle": "+14155551234", "is_me": true, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null } ], "is_group": false, "created_at": "2025-11-23T17:30:00.000Z", "updated_at": "2025-11-23T17:30:00.000Z", "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#healthy", "updated_at": "2025-11-23T17:30:00.000Z" } } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "chat.created", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "display_name": "+14155551234, +14155559876", "service": "iMessage", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440010", "handle": "+14155551234", "is_me": true, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null } ], "is_group": false, "created_at": "2025-11-23T17:30:00.000Z", "updated_at": "2025-11-23T17:30:00.000Z" } } ``` ### `chat.group_name_updated` `new_value` is `null` when the name was removed; `old_value` is `null` when no previous name existed. - [2026-02-03](#tab-panel-157) - [2025-01-01](#tab-panel-158) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "chat.group_name_updated", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "old_value": "Old Group Name", "new_value": "New Group Name", "changed_by_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "updated_at": "2025-11-23T17:50:00.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "chat.group_name_updated", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "old_value": "Old Group Name", "new_value": "New Group Name", "changed_by_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "updated_at": "2025-11-23T17:50:00.000Z" } } ``` ### `chat.group_icon_updated` Same shape as `chat.group_name_updated`; `old_value` and `new_value` are image URLs (null when absent). - [2026-02-03](#tab-panel-159) - [2025-01-01](#tab-panel-160) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "chat.group_icon_updated", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "old_value": "https://example.com/old-icon.png", "new_value": "https://example.com/new-icon.png", "changed_by_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "updated_at": "2025-11-23T17:50:00.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "chat.group_icon_updated", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "old_value": "https://example.com/old-icon.png", "new_value": "https://example.com/new-icon.png", "changed_by_handle": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": null }, "updated_at": "2025-11-23T17:50:00.000Z" } } ``` ### `chat.group_name_update_failed` Fires when a `PUT /v3/chats/{chatId}` request to change the display name couldn’t be applied. `error_code` follows the standard [error code](/error/index.md) list. - [2026-02-03](#tab-panel-161) - [2025-01-01](#tab-panel-162) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "chat.group_name_update_failed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "error_code": 3007, "failed_at": "2025-11-23T17:55:00.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "chat.group_name_update_failed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "error_code": 3007, "failed_at": "2025-11-23T17:55:00.000Z" } } ``` ### `chat.group_icon_update_failed` Identical shape to `chat.group_name_update_failed`, fired when the group icon update couldn’t be applied. - [2026-02-03](#tab-panel-163) - [2025-01-01](#tab-panel-164) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "chat.group_icon_update_failed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "error_code": 3007, "failed_at": "2025-11-23T17:55:00.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "chat.group_icon_update_failed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "error_code": 3007, "failed_at": "2025-11-23T17:55:00.000Z" } } ``` ### `chat.typing_indicator.started` Fires when a participant starts typing in a one-to-one chat. Not emitted for group chats — see [Typing Indicators](/guides/chats/typing-indicators/index.md). - [2026-02-03](#tab-panel-165) - [2025-01-01](#tab-panel-166) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "chat.typing_indicator.started", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "chat.typing_indicator.started", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ### `chat.typing_indicator.stopped` Counterpart to `chat.typing_indicator.started`. Also fires automatically when the participant sends the message they were typing. - [2026-02-03](#tab-panel-167) - [2025-01-01](#tab-panel-168) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "chat.typing_indicator.stopped", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "chat.typing_indicator.stopped", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Participant events | Event | Description | | --------------------- | ------------------------------------- | | `participant.added` | Participant added to a group chat | | `participant.removed` | Participant removed from a group chat | Participant events share the same shape across versions. ### `participant.added` Fires when a new participant joins a group chat. - [2026-02-03](#tab-panel-169) - [2025-01-01](#tab-panel-170) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "participant.added", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+14155559876", "participant": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:40:00.000Z", "left_at": null }, "added_at": "2025-11-23T17:40:00.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "participant.added", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+14155559876", "participant": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "active", "joined_at": "2025-11-23T17:40:00.000Z", "left_at": null }, "added_at": "2025-11-23T17:40:00.000Z" } } ``` ### `participant.removed` Fires when a participant leaves or is removed from a group chat. The `participant` object’s `status` is `removed` and `left_at` is set. - [2026-02-03](#tab-panel-171) - [2025-01-01](#tab-panel-172) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "participant.removed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+14155559876", "participant": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "removed", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": "2025-11-23T17:45:00.000Z" }, "removed_at": "2025-11-23T17:45:00.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "participant.removed", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-11-23T17:35:00.000Z", "trace_id": "abc123def456", "partner_id": "your-partner-id", "data": { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+14155559876", "participant": { "id": "550e8400-e29b-41d4-a716-446655440011", "handle": "+14155559876", "is_me": false, "service": "iMessage", "status": "removed", "joined_at": "2025-11-23T17:30:00.000Z", "left_at": "2025-11-23T17:45:00.000Z" }, "removed_at": "2025-11-23T17:45:00.000Z" } } ``` ## Phone number events | Event | Description | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `phone_number.status_updated` | Phone number `status` (`ACTIVE` ↔ `FLAGGED`) or `reputation` (`HEALTHY` / `AT_RISK` / `CRITICAL`) changed — see [Phone Numbers](/guides/phone-numbers/index.md) and [Phone Reputation](/guides/phone-numbers/phone-reputation/index.md) | ### `phone_number.status_updated` Fires when a phone number’s service `status` (`ACTIVE` ↔ `FLAGGED`) or `reputation` (`HEALTHY` / `AT_RISK` / `CRITICAL`) changes. Every payload carries both pairs of fields, so you can react to either signal from the same event. `previous_health_status` / `new_health_status` are **deprecated** aliases of `previous_reputation` / `new_reputation` — they carry the same value and will be removed; migrate to the `reputation` fields. - [2026-02-03](#tab-panel-173) - [2025-01-01](#tab-panel-174) ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "phone_number.status_updated", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2026-02-18T18:35:05.363Z", "trace_id": "b66e67c5c6b2c20e41d53c51698db27a", "partner_id": "your-partner-id", "data": { "phone_number": "+12025551234", "previous_status": "ACTIVE", "new_status": "FLAGGED", "previous_reputation": "AT_RISK", "new_reputation": "CRITICAL", "previous_health_status": "AT_RISK", "new_health_status": "CRITICAL", "changed_at": "2026-02-18T18:35:05.000Z" } } ``` ``` { "api_version": "v3", "webhook_version": "2025-01-01", "event_type": "phone_number.status_updated", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2026-02-18T18:35:05.363Z", "trace_id": "b66e67c5c6b2c20e41d53c51698db27a", "partner_id": "your-partner-id", "data": { "phone_number": "+12025551234", "previous_status": "ACTIVE", "new_status": "FLAGGED", "previous_reputation": "AT_RISK", "new_reputation": "CRITICAL", "previous_health_status": "AT_RISK", "new_health_status": "CRITICAL", "changed_at": "2026-02-18T18:35:05.000Z" } } ``` ## Location events | Event | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `location.sharing.started` | A participant started sharing their location with you — see [Location Sharing](/guides/location-sharing/index.md) | | `location.sharing.stopped` | A participant stopped sharing their location | Both payloads carry `shared_by` (their number) and `shared_with` (your number). `location.sharing.started` also includes `began_at`, and `ends_at` when sharing is time-boxed. After a `started` event, call [Read shared locations](/guides/location-sharing#read-shared-locations/index.md) to pull the current coordinates. ### `location.sharing.started` Fires when a participant starts sharing their location with you. ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "location.sharing.started", "event_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-02-04T12:00:00Z", "trace_id": "2eff5df5c6f688733c007523c4d61cd9", "partner_id": "your-partner-id", "data": { "shared_by": "+15551234567", "shared_with": "+15559876543", "began_at": "2026-02-04T12:00:00Z", "ends_at": "2026-02-04T13:00:00Z" } } ``` ### `location.sharing.stopped` Fires when a participant stops sharing their location. ``` { "api_version": "v3", "webhook_version": "2026-02-03", "event_type": "location.sharing.stopped", "event_id": "660f9500-f30c-52e5-b827-557766551111", "created_at": "2026-02-04T18:45:00Z", "trace_id": "3bff6ef6d7g799844d118634d5e72de0", "partner_id": "your-partner-id", "data": { "shared_by": "+15551234567", "shared_with": "+15559876543" } } ``` ## Related - [Webhooks](/guides/webhooks/index.md) — setup, signature verification, delivery guarantees - [Webhook Subscriptions](/guides/webhooks/subscriptions/index.md) — manage which events each subscription receives - [API Reference: Webhook Events](/api/resources/webhooks/index.md) — complete payload schemas for every event type --- # Webhooks URL: https://docs.linqapp.com/guides/webhooks/ Webhook Subscriptions allow you to receive real-time notifications when events occur on your account. Configure webhook endpoints to receive events such as messages sent/received, delivery status changes, reactions, typing indicators, and more. Failed deliveries (5xx, 429, network errors) are retried up to 10 times over \~25 minutes with exponential backoff. Each event includes a unique ID for deduplication. ## Webhook Headers All webhook requests include two sets of headers. **If you have an existing integration using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on every delivery and work exactly as before. The new `webhook-*` headers follow the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification. You can safely ignore them if your current verification code works and you don’t want to use this convention. ### Standard Webhooks Headers (Recommended) Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks). | Header | Description | | ------------------- | -------------------------------------------------- | | `webhook-id` | Unique event identifier (use as idempotency key) | | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent | | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) | ### Legacy Headers (Deprecated) Still sent on every delivery for backwards compatibility. Existing verification code using these headers continues to work — no changes required. | Header | Description | | --------------------------- | -------------------------------------------------- | | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) | | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID | | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) | | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) | ## Signing Secrets Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`). Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes. ## Verifying Webhook Signatures Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks). You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify signatures, or implement verification manually: **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}` **Verification Steps:** 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers 2. Reject if the timestamp is more than 5 minutes old (replay protection) 3. Get the raw request body bytes (do not parse and re-serialize) 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"` 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes 6. Compute HMAC-SHA256 using the key bytes over the signed content 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature` 8. Use constant-time comparison to prevent timing attacks **Example (Python):** ``` import base64, hmac, hashlib def verify_webhook(secret, body, headers): msg_id = headers['webhook-id'] timestamp = headers['webhook-timestamp'] signature = headers['webhook-signature'] secret_str = secret.removeprefix('whsec_') key = base64.b64decode(secret_str) signed_content = f"{msg_id}.{timestamp}.{body}" expected = base64.b64encode( hmac.new(key, signed_content.encode(), hashlib.sha256).digest() ).decode() for sig in signature.split(' '): if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]): return True return False ``` **Example (Node.js):** ``` const crypto = require('crypto'); function verifyWebhook(secret, rawBody, headers) { const msgId = headers['webhook-id']; const timestamp = headers['webhook-timestamp']; const signature = headers['webhook-signature']; const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret; const keyBytes = Buffer.from(secretStr, 'base64'); const signedContent = `${msgId}.${timestamp}.${rawBody}`; const expected = crypto .createHmac('sha256', keyBytes) .update(signedContent) .digest('base64'); return signature.split(' ').some(sig => { if (!sig.startsWith('v1,')) return false; try { return crypto.timingSafeEqual( Buffer.from(expected, 'base64'), Buffer.from(sig.slice(3), 'base64') ); } catch { return false; } }); } ``` **Security Best Practices:** - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks - Always use constant-time comparison for signature verification - Store your signing secret securely (e.g., environment variable, secrets manager) - Return a 2xx status code quickly, then process the webhook asynchronously ## Setting up webhooks Create a webhook subscription to start receiving events. Each subscription targets a URL you own, filters to the events you care about, and returns a signing secret you use to verify inbound requests. See [Webhook Subscriptions](/guides/webhooks/subscriptions/index.md) for the full subscription lifecycle — create, list, retrieve, update, delete, and phone-number filtering. ## Webhook versioning Webhook payloads are versioned using dates. Specify a version by adding `?version=YYYY-MM-DD` to your subscription URL: ``` https://your-server.com/webhook?version=2026-02-03 ``` | Subscription created | Webhook version | | -------------------- | --------------- | | Before 2026-02-03 | `2025-01-01` | | 2026-02-03 or later | `2026-02-03` | If no version is specified, the subscription uses the latest available version at creation time. > **Tip:** Always specify a version explicitly to avoid unexpected payload format changes. ## Verifying signatures with an SDK The **Verifying Webhook Signatures** section above describes the [Standard Webhooks](https://www.standardwebhooks.com/) signing scheme and how to verify it manually. If you use one of our SDKs, you don’t have to: `webhooks.unwrap()` does it for you — it checks the signature headers, throws if the signature is invalid, and returns the typed, discriminated event. Provide your signing secret via the `LINQ_WEBHOOK_SECRET` environment variable (or pass it to the client). **Always pass the raw request body — not parsed JSON — so the signature matches.** - [TypeScript](#tab-panel-137) - [Python](#tab-panel-138) - [Go](#tab-panel-139) ``` import LinqAPIV3 from '@linqapp/sdk'; // webhookSecret defaults to process.env.LINQ_WEBHOOK_SECRET const client = new LinqAPIV3(); // In your webhook handler: const event = client.webhooks.unwrap(rawBody, { headers: req.headers }); // Throws on an invalid signature. `event` is fully typed. ``` ``` from linq import LinqAPIV3 # webhook_secret defaults to os.environ["LINQ_WEBHOOK_SECRET"] client = LinqAPIV3() # In your webhook handler: event = client.webhooks.unwrap(raw_body, headers=request.headers) # Raises on an invalid signature. `event` is fully typed. ``` ``` import "github.com/linq-team/linq-go" // imported as linqgo // reads LINQ_WEBHOOK_SECRET from the environment client := linqgo.NewClient() // In your webhook handler: event, err := client.Webhooks.Unwrap(rawBody, req.Header) // err is non-nil on an invalid signature. event is the typed payload union. ``` ## Delivery guarantees | Guarantee | Value | | ------------------ | --------------------------------------------- | | Response timeout | 10 seconds | | Retry attempts | 10 per endpoint | | Retry backoff | Exponential with jitter, capped at 10 minutes | | Total retry window | \~25 minutes | | Delivery model | At-least-once (duplicates possible) | **Retried:** HTTP 5xx, HTTP 429, connection timeout, connection refused. **Not retried:** HTTP 4xx (except 429), DNS failures, invalid hostnames. Your endpoint should: 1. Return `200` quickly — process asynchronously if needed 2. [Verify the signature](#verifying-signatures-with-an-sdk) using your subscription’s signing secret 3. Deduplicate using `event_id` 4. Be idempotent ## Related - [Webhook Subscriptions](/guides/webhooks/subscriptions/index.md) — create, list, update, delete subscriptions - [Webhook Events](/guides/webhooks/events/index.md) — full event list, shared envelope, and representative payloads - [API Reference: Webhook Events](/api/resources/webhook_events/index.md) - [API Reference: Webhook Subscriptions](/api/resources/webhook_subscriptions/index.md) --- # Webhook Subscriptions URL: https://docs.linqapp.com/guides/webhooks/subscriptions/ A **webhook subscription** tells Linq to POST events to a URL you own. Each subscription has a target URL, a list of subscribed events, an optional phone-number filter, and a signing secret used to verify incoming requests. For handling events on your server (signature verification, envelope shape, event types), see [Webhooks](/guides/webhooks/index.md). ## Create a subscription Creating a subscription returns a `signing_secret` in the response — **store it securely, it cannot be retrieved later**. You use it to verify the HMAC signature on every inbound webhook. See the [Create Subscription API reference](/api/resources/webhook_subscriptions/methods/create/index.md) for the full endpoint specification. - [cURL](#tab-panel-175) - [TypeScript](#tab-panel-176) - [Python](#tab-panel-177) - [Go](#tab-panel-178) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/webhook-subscriptions \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "target_url": "https://webhooks.example.com/linq/events?version=2026-02-03", "subscribed_events": [ "message.sent", "message.delivered", "message.read" ] }' ``` ``` await client.webhookSubscriptions.create({ target_url: "https://webhooks.example.com/linq/events?version=2026-02-03", subscribed_events: ["message.sent", "message.delivered", "message.read"], }); ``` ``` client.webhookSubscriptions.create( target_url="https://webhooks.example.com/linq/events?version=2026-02-03", subscribed_events=["message.sent", "message.delivered", "message.read"], ) ``` ``` client.WebhookSubscriptions.Create(context.TODO(), linq.WebhookSubscriptionNewParams{ TargetUrl: linq.F("https://webhooks.example.com/linq/events?version=2026-02-03"), SubscribedEvents: linq.F([]string{"message.sent", "message.delivered", "message.read"}), }) ``` ### Filtering by phone number Pass a `phone_numbers` array to route only events from specific lines to this subscription. Omit it to receive events from every line on your account. - [cURL](#tab-panel-179) - [TypeScript](#tab-panel-180) - [Python](#tab-panel-181) - [Go](#tab-panel-182) Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v3/webhook-subscriptions \ -H "Authorization: Bearer $LINQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "target_url": "https://webhooks.example.com/linq/line1", "subscribed_events": [ "message.sent", "message.received" ], "phone_numbers": [ "+12025551234" ] }' ``` ``` await client.webhookSubscriptions.create({ target_url: "https://webhooks.example.com/linq/line1", subscribed_events: ["message.sent", "message.received"], phone_numbers: ["+12025551234"], }); ``` ``` client.webhookSubscriptions.create( target_url="https://webhooks.example.com/linq/line1", subscribed_events=["message.sent", "message.received"], phone_numbers=["+12025551234"], ) ``` ``` client.WebhookSubscriptions.Create(context.TODO(), linq.WebhookSubscriptionNewParams{ TargetUrl: linq.F("https://webhooks.example.com/linq/line1"), SubscribedEvents: linq.F([]string{"message.sent", "message.received"}), PhoneNumbers: linq.F([]string{"+12025551234"}), }) ``` Each `target_url` can only be used **once per account**. To route different lines to different endpoints, give each subscription a unique URL — for example by appending a query parameter (`?line=support`). ## List subscriptions Return every webhook subscription on the authenticated partner account — active and inactive. See the [List Subscriptions API reference](/api/resources/webhook_subscriptions/methods/list/index.md). ## Retrieve a subscription Fetch a single subscription by ID, including its target URL, subscribed events, phone-number filter, and active status. See the [Retrieve Subscription API reference](/api/resources/webhook_subscriptions/methods/retrieve/index.md). ## Update a subscription Change the target URL, subscribed events, phone-number filter, or active flag. Fields you omit keep their existing values. The **signing secret cannot be changed** via this endpoint — to rotate it, delete and recreate the subscription. See the [Update Subscription API reference](/api/resources/webhook_subscriptions/methods/update/index.md). Common update patterns: - **Pause a subscription** — `is_active: false`. Events stop flowing until you flip it back. - **Add or remove events** — replace `subscribed_events` with the full desired list. - **Change filter** — set `phone_numbers` to a new array, or pass an empty array / `null` to remove the filter. ## Delete a subscription Permanently remove a subscription. Events stop flowing immediately and the signing secret is discarded. See the [Delete Subscription API reference](/api/resources/webhook_subscriptions/methods/delete/index.md). ## Related - [Webhooks](/guides/webhooks/index.md) — versioning, headers, signature verification, delivery guarantees, event types - [Error 2010 — webhook subscription not found](/error/codes/2xxx/2010/index.md) - [Error 4003 — webhook delivery failed](/error/codes/4xxx/4003/index.md) - [API Reference: Webhook Subscriptions](/api/resources/webhook_subscriptions/index.md) --- # Error Codes URL: https://docs.linqapp.com/error/ All API errors return a consistent JSON envelope with a nested `error` object, a `success: false` flag, and a top-level `trace_id` for debugging. The `error` object includes a `doc_url` linking directly to the error code reference page. ``` { "success": false, "error": { "status": 400, "code": 1001, "message": "Missing required field", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1001/" }, "trace_id": "2eff5df5c6f688733c007523c4d61cd9" } ``` On `429` responses, `error` also includes a `retry_after` integer (seconds to wait before retrying). > **Tip:** Always include the `trace_id` from error responses when contacting Linq support. See [Debugging](/guides/platform/debugging/index.md) for more on trace IDs. ## Error code ranges | Range | Category | Retry? | | ------------------------------------ | ---------------------- | ----------------------------------- | | [1xxx](#1xxx--clientrequest-errors) | Client/Request Errors | No — fix the request | | [2xxx](#2xxx--resource-errors) | Resource Errors | No — fix auth or resource reference | | [3xxx](#3xxx--server-errors) | Server Errors | Yes — retry with backoff | | [4xxx](#4xxx--delivery-errors) | Delivery Errors | Sometimes — depends on cause | | [5xxx](#5xxx--attachmentfile-errors) | Attachment/File Errors | Sometimes — depends on cause | ## 1xxx — Client/Request Errors | Code | Message | HTTP | Troubleshooting | | --------------------------------------- | ------------------------------------ | ---- | -------------------------------------------------------------------------------------------------------------- | | [1001](/error/codes/1xxx/1001/index.md) | Missing required field | 400 | Check API docs for required fields. | | [1002](/error/codes/1xxx/1002/index.md) | Phone number must be in E.164 format | 400 | Include country code with `+` prefix (e.g., `+14155551234`). | | [1003](/error/codes/1xxx/1003/index.md) | Invalid request body | 400 | Validate JSON syntax. Set `Content-Type: application/json`. | | [1004](/error/codes/1xxx/1004/index.md) | Invalid message content | 400 | Ensure parts array has valid `text`, `media`, or `link` parts. A `link` part must be the only part. | | [1005](/error/codes/1xxx/1005/index.md) | Invalid parameter value | 400 | Review parameter against API spec. | | [1006](/error/codes/1xxx/1006/index.md) | Cannot update direct message chats | 409 | Only [group chats](/guides/chats/group-chats/index.md) support updates. | | [1007](/error/codes/1xxx/1007/index.md) | Rate limit exceeded | 429 | Wait for reset or contact support for higher limits. See [Rate Limits](/guides/platform/rate-limits/index.md). | ## 2xxx — Resource Errors | Code | Message | HTTP | Troubleshooting | | --------------------------------------- | -------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | [2001](/error/codes/2xxx/2001/index.md) | Chat not found | 404 | Verify chat ID is correct UUID from `POST`/`GET /v3/chats`. | | [2002](/error/codes/2xxx/2002/index.md) | Message not found | 404 | Verify message ID and chat access. | | [2003](/error/codes/2xxx/2003/index.md) | Attachment not found | 404 | Verify attachment ID. Presigned URLs expire — ensure uploads complete in time. | | [2004](/error/codes/2xxx/2004/index.md) | Unauthorized | 401 | Include valid Bearer token in Authorization header. See [Authentication](/getting-started/authentication/index.md). | | [2005](/error/codes/2xxx/2005/index.md) | Access denied | 403 | You do not have permission to access this resource. | | [2006](/error/codes/2xxx/2006/index.md) | Phone number permission denied | 403 | Verify the phone is assigned to your account. | | [2007](/error/codes/2xxx/2007/index.md) | Attachment not ready | 404 | Attachment is still processing. Wait a few seconds and retry. | | [2008](/error/codes/2xxx/2008/index.md) | Recipient not allowed | 403 | In sandbox, recipients must message you first. | | [2009](/error/codes/2xxx/2009/index.md) | The chat is still being created | 409 | Wait a few seconds and retry. | | [2010](/error/codes/2xxx/2010/index.md) | Webhook subscription not found | 404 | Verify subscription ID from `POST`/`GET /v3/webhook-subscriptions`. | | [2011](/error/codes/2xxx/2011/index.md) | Feature not available | 403 | Contact support to enable this feature. | | [2012](/error/codes/2xxx/2012/index.md) | Contact card not found | 404 | No active contact card for this phone number. | | [2013](/error/codes/2xxx/2013/index.md) | This chat is unavailable | 409 | Chat may have been left or deleted. You cannot interact with it after leaving. | | [2014](/error/codes/2xxx/2014/index.md) | Contact card already exists | 409 | A contact card already exists for this phone number. Use `PATCH` to update it. | | [2015](/error/codes/2xxx/2015/index.md) | Operation conflicts with current state | 409 | The resource is in a state that conflicts with this operation (for example, editing a message that has already been deleted). Refresh state and retry. | ## 3xxx — Server Errors These are transient errors. Retry with exponential backoff (start at 1 second, max 30 seconds). The official [SDKs](/getting-started/sdks/index.md) handle retries automatically. | Code | Message | HTTP | Troubleshooting | | --------------------------------------- | ---------------------------------- | ---- | ------------------------------------------------------------------------ | | [3001](/error/codes/3xxx/3001/index.md) | Server connection error | 500 | Retry after 1-5 seconds. | | [3002](/error/codes/3xxx/3002/index.md) | Server operation failed | 500 | Retry after 1-5 seconds. If persistent, contact support with `trace_id`. | | [3003](/error/codes/3xxx/3003/index.md) | Service connection error | 500 | Retry after 1-5 seconds. | | [3004](/error/codes/3xxx/3004/index.md) | Service operation failed | 500 | Retry after 1-5 seconds. If persistent, contact support with `trace_id`. | | [3005](/error/codes/3xxx/3005/index.md) | Network timeout | 504 | Retry after a short delay. | | [3006](/error/codes/3xxx/3006/index.md) | Internal server error | 500 | If persistent, contact support with `trace_id`. | | [3007](/error/codes/3xxx/3007/index.md) | Maximum delivery attempts exceeded | 500 | Check recipient availability and try again later. | ## 4xxx — Delivery Errors | Code | Message | HTTP | Troubleshooting | | --------------------------------------- | ----------------------- | ---- | --------------------------------------------------------------------------------------------------- | | [4001](/error/codes/4xxx/4001/index.md) | Delivery failed | 500 | Try sending again. If persistent, contact support with `trace_id`. | | [4002](/error/codes/4xxx/4002/index.md) | Phone not available | 500 | Check phone connectivity status. | | [4003](/error/codes/4xxx/4003/index.md) | Webhook delivery failed | 500 | Ensure endpoint is reachable and returns 2xx within 10s. See [Webhooks](/guides/webhooks/index.md). | | [4004](/error/codes/4xxx/4004/index.md) | Service unavailable | 503 | Retry after 30 seconds. | ## 5xxx — Attachment/File Errors | Code | Message | HTTP | Troubleshooting | | --------------------------------------- | ---------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- | | [5001](/error/codes/5xxx/5001/index.md) | File upload failed | 500 | Retry the upload. Check file size limits. See [Attachments](/guides/messaging/attachments/index.md). | | [5002](/error/codes/5xxx/5002/index.md) | File download failed | 500 | Ensure URL is public, HTTPS, with valid SSL. | | [5003](/error/codes/5xxx/5003/index.md) | Failed to generate file URL | 500 | Retry the request. | | [5004](/error/codes/5xxx/5004/index.md) | Invalid file type | 400 | Supported types include JPEG, PNG, GIF, MP4, PDF. See [supported types](/guides/messaging/attachments#supported-file-types/index.md). | | [5005](/error/codes/5xxx/5005/index.md) | File too large | 400 | Reduce or compress file before uploading. See [file size limits](/guides/messaging/attachments#file-size-limits/index.md). | | [5006](/error/codes/5xxx/5006/index.md) | Content type mismatch | 400 | Ensure URL extension matches actual file type, or use [pre-upload](/guides/messaging/attachments#pre-upload-up-to-100mb/index.md). | | [5007](/error/codes/5xxx/5007/index.md) | Failed to download image from the provided URL | 400 | Ensure the URL is publicly accessible and returns a valid image. | --- # 1001: Missing required field URL: https://docs.linqapp.com/error/codes/1xxx/1001/ A required field is missing from the request body or query parameters. This is an HTTP 400 error indicating the request cannot be processed as-is. ## Troubleshooting - Check the API docs for the required fields on the endpoint you are calling - Use an [official SDK](/getting-started/sdks/index.md) which enforces required fields at compile time ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [API reference: Chats](/api/resources/chats/index.md) - [SDKs](/getting-started/sdks/index.md) --- # 1002: Phone number must be in E.164 format URL: https://docs.linqapp.com/error/codes/1xxx/1002/ Phone numbers must conform to E.164 format (e.g., `+14155551234`). This is an HTTP 400 error indicating the phone number in your request is not properly formatted. ## Troubleshooting - Include the country code with a `+` prefix (e.g., `+14155551234` for a US number) - Remove any spaces, dashes, or parentheses from the phone number ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [API reference: Chats](/api/resources/chats/index.md) --- # 1003: Invalid request body URL: https://docs.linqapp.com/error/codes/1xxx/1003/ The request body is malformed or invalid JSON. This is an HTTP 400 error indicating the server could not parse your request. ## Troubleshooting - Validate your JSON syntax using a linter or validator - Set the `Content-Type` header to `application/json` ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [Debugging](/guides/platform/debugging/index.md) - [SDKs](/getting-started/sdks/index.md) --- # 1004: Invalid message content URL: https://docs.linqapp.com/error/codes/1xxx/1004/ The message content or parts array is invalid. This is an HTTP 400 error indicating the message you are trying to send has an invalid structure. ## Troubleshooting - Ensure the `parts` array contains valid `text`, `media`, or `link` parts - A `link` part must be the only part in the message — it cannot be combined with other parts ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Chats](/api/resources/chats/index.md) --- # 1005: Invalid parameter value URL: https://docs.linqapp.com/error/codes/1xxx/1005/ A parameter value does not meet validation requirements. This is an HTTP 400 error indicating one of the values in your request is outside the accepted range or format. ## Troubleshooting - Review the parameter against the API spec to confirm accepted values, formats, and constraints ## Related - [Debugging](/guides/platform/debugging/index.md) - [SDKs](/getting-started/sdks/index.md) --- # 1006: Cannot update direct message chats URL: https://docs.linqapp.com/error/codes/1xxx/1006/ Direct message chats cannot be updated. Only group chats support updates. This is an HTTP 409 error indicating you attempted to modify a direct message chat, which is not supported. ## Troubleshooting - Only group chats support updates — verify the chat you are trying to update is a group chat, not a direct message ## Related - [Group chats](/guides/chats/group-chats/index.md) - [API reference: Chats](/api/resources/chats/index.md) --- # 1007: Rate limit exceeded URL: https://docs.linqapp.com/error/codes/1xxx/1007/ HTTP `429 Too Many Requests`. The response includes a `Retry-After` header (and matching `retry_after` field in the error body) with the number of seconds to wait before the next request. There are three situations that produce this code: - **Per-phone-pair limit** — more than 30 messages in a 60-second window between the same sender and recipient. - **Sandbox daily limit** — 100 messages per day on a [sandbox account](https://dashboard.linqapp.com/sandbox-signup/), resetting at midnight UTC. - **Capability check limit** — too many calls to `/v3/capability/check_imessage` or `/v3/capability/check_rcs`. Cache results briefly instead of re-checking on every send. ## Troubleshooting - Respect the `Retry-After` interval before retrying the request. - If you’re hitting the per-pair limit, check for accidental retry loops, duplicate sends, or webhook handlers echoing back to the sender — 30 messages in 60 seconds between the same two numbers almost always indicates a bug. - If you hit the sandbox daily limit, wait for the midnight UTC reset or [request production access](https://dashboard.linqapp.com/sandbox-signup/). - Capability checks are limited to 1 per 10 seconds. For capability check limits, cache results for minutes rather than re-checking the same address on every send. See [Capability Checks → Caching results](/guides/chats/capability-checks#caching-results/index.md). ## Related - [Rate limits](/guides/platform/rate-limits/index.md) - [Capability checks](/guides/chats/capability-checks/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2001: Chat not found URL: https://docs.linqapp.com/error/codes/2xxx/2001/ The requested chat does not exist or you lack permission. This is an HTTP 404 error indicating the chat ID you provided could not be resolved. ## Troubleshooting - Verify the chat ID is a correct UUID returned from `POST /v3/chats` or `GET /v3/chats` ## Related - [API reference: Chats](/api/resources/chats/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2002: Message not found URL: https://docs.linqapp.com/error/codes/2xxx/2002/ The requested message does not exist or you lack permission. This is an HTTP 404 error indicating the message ID you provided could not be resolved. ## Troubleshooting - Verify the message ID is correct - Verify you have access to the chat that contains the message ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [API reference: Chats](/api/resources/chats/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2003: Attachment not found URL: https://docs.linqapp.com/error/codes/2xxx/2003/ The requested attachment does not exist or is no longer available. This is an HTTP 404 error indicating the attachment ID you provided could not be resolved. ## Troubleshooting - Verify the attachment ID is correct - Presigned upload and download URLs expire, so ensure uploads complete before the URL expires ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2004: Unauthorized URL: https://docs.linqapp.com/error/codes/2xxx/2004/ Missing or invalid authentication credentials. This is an HTTP 401 error indicating your request did not include a valid authentication token. ## Troubleshooting - Include a valid Bearer token in the `Authorization` header (e.g., `Authorization: Bearer your-api-key`) ## Related - [Authentication](/getting-started/authentication/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2005: Access denied URL: https://docs.linqapp.com/error/codes/2xxx/2005/ You do not have permission to access this resource. This is an HTTP 403 error indicating your credentials are valid but you are not authorized to perform this action. ## Troubleshooting - Verify the resource (chat, attachment, subscription) belongs to your partner account. - Confirm the API key you’re using has access to the phone number involved in the request — see [Phone number permission denied (2006)](/error/codes/2xxx/2006/index.md) if the resource is a phone line. - If you rotated keys, make sure the new key has the same scope as the old one. ## Related - [Authentication](/getting-started/authentication/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2006: Phone number permission denied URL: https://docs.linqapp.com/error/codes/2xxx/2006/ You cannot send from this phone number. This is an HTTP 403 error indicating the phone number you specified in the `from` field is not authorized for your account. ## Troubleshooting - Verify the phone number is assigned to your account ## Related - [Authentication](/getting-started/authentication/index.md) - [Sending messages](/guides/messaging/sending-messages/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2007: Attachment not ready URL: https://docs.linqapp.com/error/codes/2xxx/2007/ Attachment is still processing. This is an HTTP 404 error indicating the attachment you referenced has not finished being processed by the server. ## Troubleshooting - Wait a few seconds and retry the request ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) --- # 2008: Recipient not allowed URL: https://docs.linqapp.com/error/codes/2xxx/2008/ Recipient is not in the allowed list for this account. This is an HTTP 403 error indicating you cannot send messages to this recipient. ## Troubleshooting - In sandbox mode, recipients must message you first before you can send to them ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2009: The chat is still being created URL: https://docs.linqapp.com/error/codes/2xxx/2009/ The chat is still being created. This is an HTTP 409 error indicating the chat you are trying to interact with has not finished being provisioned. ## Troubleshooting - Wait a few seconds and retry the request ## Related - [API reference: Chats](/api/resources/chats/index.md) - [Sending messages](/guides/messaging/sending-messages/index.md) --- # 2010: Webhook subscription not found URL: https://docs.linqapp.com/error/codes/2xxx/2010/ The requested webhook subscription does not exist. This is an HTTP 404 error indicating the subscription ID you provided could not be resolved. ## Troubleshooting - Verify the subscription ID is a correct UUID returned from `POST /v3/webhook-subscriptions` or `GET /v3/webhook-subscriptions` ## Related - [Webhooks](/guides/webhooks/index.md) - [API reference: Webhook subscriptions](/api/resources/webhook_subscriptions/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2011: Feature not available URL: https://docs.linqapp.com/error/codes/2xxx/2011/ The requested feature is not available for your account. This is an HTTP 403 error indicating the feature you are trying to use has not been enabled. ## Troubleshooting - Contact support to enable this feature for your account ## Related - [Authentication](/getting-started/authentication/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2012: Contact card not found URL: https://docs.linqapp.com/error/codes/2xxx/2012/ No active contact card was found for this phone number. This is an HTTP 404 error indicating there is no contact card associated with the phone number you specified. ## Troubleshooting - Verify the phone number is in E.164 format and matches a line assigned to your account. - Create a contact card for this number before trying to retrieve or update it — see [Contact Cards](/guides/contact-cards/index.md). ## Related - [Contact Cards](/guides/contact-cards/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2013: This chat is unavailable URL: https://docs.linqapp.com/error/codes/2xxx/2013/ This chat is unavailable. The chat may have been left, deleted, or is otherwise inaccessible. This is an HTTP 409 error indicating you cannot interact with this chat. ## Troubleshooting - Check the chat status — you cannot interact with or perform actions on a chat after leaving it ## Related - [API reference: Chats](/api/resources/chats/index.md) - [Group chats](/guides/chats/group-chats/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2014: Contact card already exists URL: https://docs.linqapp.com/error/codes/2xxx/2014/ A contact card already exists for this phone number. This is an HTTP 409 error indicating you attempted to create a duplicate contact card. ## Troubleshooting - Use `PATCH /v3/contact_card/{phone_number}` to update the existing card instead of `POST` to create a new one. - If you need to start over, delete the existing card first, then create a new one. ## Related - [Contact Cards](/guides/contact-cards/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 2015: Operation conflicts with current state URL: https://docs.linqapp.com/error/codes/2xxx/2015/ The resource is in a state that conflicts with the requested operation. This is an HTTP 409 error. Common causes include editing or reacting to a message that has been deleted, or mutating a chat that is in a transitional state. ## Troubleshooting - Refresh the resource before retrying (e.g., re-fetch the chat or message) - If you are racing a delete or status change, wait for the relevant webhook before retrying ## Related - [Contact Cards](/guides/contact-cards/index.md) - [Debugging](/guides/platform/debugging/index.md) - [Error Codes](/error/index.md) --- # 2018: iMessage app messages can only be sent over iMessage URL: https://docs.linqapp.com/error/codes/2xxx/2018/ iMessage app messages can only be sent over iMessage. The request included an `imessage_app` part while explicitly requesting SMS or RCS. This is an HTTP 409 error. ## Troubleshooting - Omit `preferred_service` — it defaults to iMessage for app parts — or set it to `iMessage` - iMessage app parts never fall back to SMS/RCS. If the recipient isn’t reachable over iMessage, the send instead fails asynchronously with a `message.failed` webhook carrying code [4005](/error/codes/4xxx/4005/index.md) - [Check iMessage capability](/guides/messaging/protocol-selection#protocol-capabilities/index.md) before sending ## Related - [iMessage Apps](/guides/messaging/imessage-apps/index.md) - [Protocol Selection](/guides/messaging/protocol-selection/index.md) - [Error Codes](/error/index.md) --- # 3001: Server connection error URL: https://docs.linqapp.com/error/codes/3xxx/3001/ Transient server connection error. This is an HTTP 500 error indicating the server encountered a temporary connectivity issue. ## Troubleshooting - Retry the request after 1-5 seconds ## Related - [Debugging](/guides/platform/debugging/index.md) --- # 3002: Server operation failed URL: https://docs.linqapp.com/error/codes/3xxx/3002/ Server operation failed unexpectedly. This is an HTTP 500 error indicating the server encountered an unexpected failure while processing your request. ## Troubleshooting - Retry the request after 1-5 seconds - If the error persists, contact support with the `trace_id` from the error response ## Related - [Debugging](/guides/platform/debugging/index.md) --- # 3003: Service connection error URL: https://docs.linqapp.com/error/codes/3xxx/3003/ Transient internal service connection error. This is an HTTP 500 error indicating a temporary connectivity issue between internal services. ## Troubleshooting - Retry the request after 1-5 seconds ## Related - [Debugging](/guides/platform/debugging/index.md) --- # 3004: Service operation failed URL: https://docs.linqapp.com/error/codes/3xxx/3004/ Internal service operation failed. This is an HTTP 500 error indicating an internal service encountered an unexpected failure. ## Troubleshooting - Retry the request after 1-5 seconds - If the error persists, contact support with the `trace_id` from the error response ## Related - [Debugging](/guides/platform/debugging/index.md) --- # 3005: Network timeout URL: https://docs.linqapp.com/error/codes/3xxx/3005/ Request timed out. This is an HTTP 504 error indicating the server did not receive a timely response from an upstream service. ## Troubleshooting - Retry the request after a short delay ## Related - [Debugging](/guides/platform/debugging/index.md) --- # 3006: Internal server error URL: https://docs.linqapp.com/error/codes/3xxx/3006/ Unexpected internal error. This is an HTTP 500 error indicating the server encountered an unhandled failure. ## Troubleshooting - If the error persists, contact support with the `trace_id` from the error response ## Related - [Debugging](/guides/platform/debugging/index.md) --- # 3007: Maximum delivery attempts exceeded URL: https://docs.linqapp.com/error/codes/3xxx/3007/ All delivery retry attempts exhausted. This is an HTTP 500 error indicating the server tried multiple times to deliver your message but was unable to complete delivery. ## Troubleshooting - Check recipient availability and try again later ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 4001: Delivery failed URL: https://docs.linqapp.com/error/codes/4xxx/4001/ Message could not be delivered. This may be due to an unreachable phone, service disruption, or exhausted retry attempts. This is an HTTP 500 error. ## Troubleshooting - Try sending the message again - If the problem persists, contact support with the `trace_id` from the error response ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 4002: Phone not available URL: https://docs.linqapp.com/error/codes/4xxx/4002/ Phone is offline or unreachable. This is an HTTP 500 error indicating the **sender** line on your account is not currently connected. ## Troubleshooting - Check the status of the sender line in your dashboard. - Retry the request after a short delay — the line may come back online automatically. - If the line stays offline, subscribe to the `phone_number.status_updated` webhook to be notified when it reconnects. ## Related - [Sending messages](/guides/messaging/sending-messages/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 4003: Webhook delivery failed URL: https://docs.linqapp.com/error/codes/4xxx/4003/ Failed to deliver webhook to your endpoint. This is an HTTP 500 error indicating the server was unable to reach your webhook URL. ## Troubleshooting - Ensure your endpoint is publicly reachable - Ensure your endpoint returns a 2xx status code within 10 seconds ## Related - [Webhooks](/guides/webhooks/index.md) - [API reference: Webhook subscriptions](/api/resources/webhook_subscriptions/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 4004: Service unavailable URL: https://docs.linqapp.com/error/codes/4xxx/4004/ External service temporarily unavailable. This is an HTTP 503 error indicating a downstream service is not currently responding. ## Troubleshooting - Retry the request after 30 seconds ## Related - [Debugging](/guides/platform/debugging/index.md) --- # 4005: Recipient does not support this message type URL: https://docs.linqapp.com/error/codes/4xxx/4005/ The recipient cannot receive this message type. iMessage app messages require the recipient to be reachable over iMessage and do not fall back to SMS/RCS. This is an HTTP 422 error, delivered asynchronously as a `message.failed` webhook. ## Troubleshooting - Confirm the recipient is iMessage-capable before sending app messages — see the [iMessage capability check](/guides/messaging/protocol-selection#protocol-capabilities/index.md) - Or send a standard text or media message, which can fall back to SMS/RCS ## Related - [iMessage Apps](/guides/messaging/imessage-apps/index.md) - [Webhook Events](/guides/webhooks/events/index.md) - [Error Codes](/error/index.md) --- # 5001: File upload failed URL: https://docs.linqapp.com/error/codes/5xxx/5001/ File upload failed due to network or storage issues. This is an HTTP 500 error indicating the file could not be stored. ## Troubleshooting - Retry the upload (or the message that contained the attachment) - Check file size limits ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 5002: File download failed URL: https://docs.linqapp.com/error/codes/5xxx/5002/ Failed to download file from URL. This is an HTTP 500 error indicating the server could not retrieve the file at the URL you provided. ## Troubleshooting - Ensure the URL is publicly accessible - Ensure the URL uses HTTPS with a valid SSL certificate ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 5003: Failed to generate file URL URL: https://docs.linqapp.com/error/codes/5xxx/5003/ Failed to generate presigned URL for file access. This is an HTTP 500 error indicating the server could not create a temporary URL for uploading or downloading the file. ## Troubleshooting - Retry the request ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # 5004: Invalid file type URL: https://docs.linqapp.com/error/codes/5xxx/5004/ File type not supported. This is an HTTP 400 error indicating the file you uploaded or referenced is not an accepted format. ## Troubleshooting - Supported file types include JPEG, PNG, GIF, MP4, and PDF ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) --- # 5005: File too large URL: https://docs.linqapp.com/error/codes/5xxx/5005/ File exceeds maximum size. This is an HTTP 400 error indicating the file you are trying to upload is larger than the allowed limit. ## Troubleshooting - Reduce or compress the file before uploading ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) --- # 5006: Content type mismatch URL: https://docs.linqapp.com/error/codes/5xxx/5006/ File content type does not match URL extension. This is an HTTP 400 error indicating the MIME type of the file does not agree with the file extension in the URL. ## Troubleshooting - Ensure the URL extension matches the actual file type - Alternatively, download the file and upload it using a presigned URL ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) --- # 5007: Failed to download image from the provided URL URL: https://docs.linqapp.com/error/codes/5xxx/5007/ The provided image URL could not be fetched. The URL may be unreachable, require authentication, or have timed out. This is an HTTP 400 error. ## Troubleshooting - Ensure the URL is publicly accessible and returns a valid image ## Related - [Attachments](/guides/messaging/attachments/index.md) - [API reference: Attachments](/api/resources/attachments/index.md) - [Debugging](/guides/platform/debugging/index.md) --- # API Reference URL: https://docs.linqapp.com/api/ ## [Shared](/api/resources/$shared/index.md) ## [Chats](/api/resources/chats/index.md) - [Create](/api/resources/chats/methods/create/index.md) - [List Chats](/api/resources/chats/methods/list_chats/index.md) - [Retrieve](/api/resources/chats/methods/retrieve/index.md) - [Update](/api/resources/chats/methods/update/index.md) - [Mark As Read](/api/resources/chats/methods/mark_as_read/index.md) - [Leave Chat](/api/resources/chats/methods/leave_chat/index.md) - [Share Contact Card](/api/resources/chats/methods/share_contact_card/index.md) - [Send Voicememo](/api/resources/chats/methods/send_voicememo/index.md) - [Participants](/api/resources/chats/subresources/participants/index.md) - [Add](/api/resources/chats/subresources/participants/methods/add/index.md) - [Remove](/api/resources/chats/subresources/participants/methods/remove/index.md) - [Typing](/api/resources/chats/subresources/typing/index.md) - [Start](/api/resources/chats/subresources/typing/methods/start/index.md) - [Stop](/api/resources/chats/subresources/typing/methods/stop/index.md) - [Messages](/api/resources/chats/subresources/messages/index.md) - [Send](/api/resources/chats/subresources/messages/methods/send/index.md) - [List](/api/resources/chats/subresources/messages/methods/list/index.md) - [Location](/api/resources/chats/subresources/location/index.md) - [Request](/api/resources/chats/subresources/location/methods/request/index.md) - [Retrieve](/api/resources/chats/subresources/location/methods/retrieve/index.md) ## [Messages](/api/resources/messages/index.md) - [Create](/api/resources/messages/methods/create/index.md) - [List Messages Thread](/api/resources/messages/methods/list_messages_thread/index.md) - [Retrieve](/api/resources/messages/methods/retrieve/index.md) - [Delete](/api/resources/messages/methods/delete/index.md) - [Add Reaction](/api/resources/messages/methods/add_reaction/index.md) - [Update](/api/resources/messages/methods/update/index.md) - [Update App Card](/api/resources/messages/methods/update_app_card/index.md) ## [Attachments](/api/resources/attachments/index.md) - [Create](/api/resources/attachments/methods/create/index.md) - [Retrieve](/api/resources/attachments/methods/retrieve/index.md) - [Delete](/api/resources/attachments/methods/delete/index.md) ## [Phonenumbers](/api/resources/phonenumbers/index.md) - [List](/api/resources/phonenumbers/methods/list/index.md) ## [Phone Numbers](/api/resources/phone_numbers/index.md) - [List](/api/resources/phone_numbers/methods/list/index.md) - [Update](/api/resources/phone_numbers/methods/update/index.md) ## [Available Number](/api/resources/available_number/index.md) - [Retrieve](/api/resources/available_number/methods/retrieve/index.md) ## [Webhook Events](/api/resources/webhook_events/index.md) - [List](/api/resources/webhook_events/methods/list/index.md) ## [Webhook Subscriptions](/api/resources/webhook_subscriptions/index.md) - [Create](/api/resources/webhook_subscriptions/methods/create/index.md) - [List](/api/resources/webhook_subscriptions/methods/list/index.md) - [Retrieve](/api/resources/webhook_subscriptions/methods/retrieve/index.md) - [Update](/api/resources/webhook_subscriptions/methods/update/index.md) - [Delete](/api/resources/webhook_subscriptions/methods/delete/index.md) ## [Capability](/api/resources/capability/index.md) - [Check IMessage](/api/resources/capability/methods/check_imessage/index.md) - [Check RCS](/api/resources/capability/methods/check_rcs/index.md) ## [Webhooks](/api/resources/webhooks/index.md) - [Unwrap](/api/resources/webhooks/methods/unwrap/index.md) ## [Contact Card](/api/resources/contact_card/index.md) - [Retrieve](/api/resources/contact_card/methods/retrieve/index.md) - [Create](/api/resources/contact_card/methods/create/index.md) - [Update](/api/resources/contact_card/methods/update/index.md) --- # Attachments URL: https://docs.linqapp.com/api/resources/attachments/ ## Pre-upload a file **post** `/v3/attachments` **This endpoint is optional.** You can send media by simply providing a URL in your message's media part — no pre-upload required. Use this endpoint only when you want to upload a file ahead of time for reuse or latency optimization. Returns a presigned upload URL and a permanent `attachment_id` you can reference in future messages. ## Step 1: Request an upload URL Call this endpoint with file metadata: ```json POST /v3/attachments { "filename": "photo.jpg", "content_type": "image/jpeg", "size_bytes": 1024000 } ``` The response includes an `upload_url` (valid for 15 minutes) and a permanent `attachment_id`. ## Step 2: Upload the file Make a PUT request to the `upload_url` with the raw file bytes as the request body. You **must** include all headers from `required_headers` exactly as returned — the presigned URL is signed with these values and S3 will reject the upload if they don't match. The request body is the binary file content — **not** JSON, **not** multipart form data. The file must equal `size_bytes` bytes (the value you declared in step 1). ```bash curl -X PUT "<upload_url from step 1>" \ -H "Content-Type: image/jpeg" \ -H "Content-Length: 1024000" \ --data-binary @photo.jpg ``` ## Step 3: Send a message with the attachment Reference the `attachment_id` in a media part. The ID never expires — use it in as many messages as you want. ```json POST /v3/chats { "from": "+15559876543", "to": ["+15551234567"], "message": { "parts": [ { "type": "media", "attachment_id": "<attachment_id from step 1>" } ] } } ``` ## When to use this instead of a URL in the media part - Sending the same file to multiple recipients (avoids re-downloading each time) - Large files where you want to separate upload from message send - Latency-sensitive sends where the file should already be stored If you just need to send a file once, skip all of this and pass a `url` directly in the media part instead. **File Size Limit:** 100MB **Unsupported Types:** WebP, SVG, FLAC, OGG, and executable files are explicitly rejected. ### Body Parameters - `content_type: SupportedContentType` Supported MIME types for file attachments and media URLs. **Images:** image/jpeg, image/png, image/gif, image/heic, image/heif, image/tiff, image/bmp, image/svg+xml, image/webp, image/x-icon **Videos:** video/mp4, video/quicktime, video/mpeg, video/mpeg2, video/x-msvideo, video/3gpp **Audio:** audio/mpeg, audio/x-m4a, audio/x-caf, audio/x-wav, audio/x-aiff, audio/aac, audio/midi, audio/amr **Documents:** application/pdf, text/plain, text/markdown, text/vcard, text/rtf, text/csv, text/html, text/calendar, text/xml, application/json, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/x-iwork-pages-sffpages, application/x-iwork-numbers-sffnumbers, application/x-iwork-keynote-sffkey, application/epub+zip, application/zip, application/x-gzip **Transcoded on delivery:** - `audio/x-caf` — CAF files are transcoded to `audio/mp4` for delivery. **Deprecated (accepted but transcoded):** - `audio/mp3` — Deprecated. Use `audio/mpeg` instead. Files sent as audio/mp3 will be delivered as audio/mpeg. - `audio/mp4` — Deprecated. Use `audio/x-m4a` instead. Files sent as audio/mp4 will be delivered as audio/x-m4a. - `audio/aiff` — Deprecated. Use `audio/x-aiff` instead. Files sent as audio/aiff will be delivered as audio/x-aiff. - `image/tiff` — Accepted, but TIFF images are transcoded to JPEG for delivery. **Unsupported:** FLAC, OGG, and executable files are explicitly rejected. - `"image/jpeg"` - `"image/png"` - `"image/gif"` - `"image/heic"` - `"image/heif"` - `"image/tiff"` - `"image/bmp"` - `"image/svg+xml"` - `"image/webp"` - `"image/x-icon"` - `"video/mp4"` - `"video/quicktime"` - `"video/mpeg"` - `"video/mpeg2"` - `"video/x-m4v"` - `"video/x-msvideo"` - `"video/3gpp"` - `"audio/mpeg"` - `"audio/mp3"` - `"audio/x-m4a"` - `"audio/mp4"` - `"audio/x-caf"` - `"audio/x-wav"` - `"audio/x-aiff"` - `"audio/aiff"` - `"audio/aac"` - `"audio/midi"` - `"audio/amr"` - `"application/pdf"` - `"text/plain"` - `"text/markdown"` - `"text/vcard"` - `"text/rtf"` - `"text/csv"` - `"text/html"` - `"text/calendar"` - `"application/msword"` - `"application/vnd.openxmlformats-officedocument.wordprocessingml.document"` - `"application/vnd.ms-excel"` - `"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"` - `"application/vnd.ms-powerpoint"` - `"application/vnd.openxmlformats-officedocument.presentationml.presentation"` - `"application/x-iwork-pages-sffpages"` - `"application/x-iwork-numbers-sffnumbers"` - `"application/x-iwork-keynote-sffkey"` - `"application/epub+zip"` - `"text/xml"` - `"application/json"` - `"application/zip"` - `"application/x-gzip"` - `filename: string` Name of the file to upload - `size_bytes: number` Size of the file in bytes (max 100MB) ### Returns - `attachment_id: string` Unique identifier for the attachment - `download_url: string` Permanent CDN URL for the file. Does not expire. Use the `attachment_id` to reference this file in media parts when sending messages. - `expires_at: string` When the upload URL expires (15 minutes from now) - `http_method: "PUT"` HTTP method to use for upload (always PUT) - `"PUT"` - `required_headers: map[string]` HTTP headers that must be set on the upload request. The presigned URL is signed with these exact values — S3 will reject the upload if they don't match. - `upload_url: string` Presigned URL for uploading the file. PUT the raw binary file content to this URL with the `required_headers`. Do not JSON-encode or multipart-wrap the body. Expires after 15 minutes. ### Example ```http curl https://api.linqapp.com/api/partner/v3/attachments \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "content_type": "image/jpeg", "filename": "photo.jpg", "size_bytes": 1024000 }' ``` #### Response ```json { "attachment_id": "550e8400-e29b-41d4-a716-446655440000", "upload_url": "https://uploads.linqapp.com/attachments/550e8400?X-Amz-Algorithm=AWS4-HMAC-SHA256&...", "download_url": "https://cdn.linqapp.com/uploads/partner-id/550e8400/photo.jpg", "http_method": "PUT", "expires_at": "2024-01-15T10:45:00Z", "required_headers": { "Content-Type": "image/jpeg", "Content-Length": "1024000" } } ``` ## Get attachment metadata **get** `/v3/attachments/{attachmentId}` Retrieve metadata for a specific attachment including file information, and URLs for downloading. `status`: (**deprecated** — will be removed in a future API version) ### Path Parameters - `attachmentId: string` ### Returns - `id: string` Unique identifier for the attachment (UUID) - `content_type: SupportedContentType` Supported MIME types for file attachments and media URLs. **Images:** image/jpeg, image/png, image/gif, image/heic, image/heif, image/tiff, image/bmp, image/svg+xml, image/webp, image/x-icon **Videos:** video/mp4, video/quicktime, video/mpeg, video/mpeg2, video/x-msvideo, video/3gpp **Audio:** audio/mpeg, audio/x-m4a, audio/x-caf, audio/x-wav, audio/x-aiff, audio/aac, audio/midi, audio/amr **Documents:** application/pdf, text/plain, text/markdown, text/vcard, text/rtf, text/csv, text/html, text/calendar, text/xml, application/json, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/x-iwork-pages-sffpages, application/x-iwork-numbers-sffnumbers, application/x-iwork-keynote-sffkey, application/epub+zip, application/zip, application/x-gzip **Transcoded on delivery:** - `audio/x-caf` — CAF files are transcoded to `audio/mp4` for delivery. **Deprecated (accepted but transcoded):** - `audio/mp3` — Deprecated. Use `audio/mpeg` instead. Files sent as audio/mp3 will be delivered as audio/mpeg. - `audio/mp4` — Deprecated. Use `audio/x-m4a` instead. Files sent as audio/mp4 will be delivered as audio/x-m4a. - `audio/aiff` — Deprecated. Use `audio/x-aiff` instead. Files sent as audio/aiff will be delivered as audio/x-aiff. - `image/tiff` — Accepted, but TIFF images are transcoded to JPEG for delivery. **Unsupported:** FLAC, OGG, and executable files are explicitly rejected. - `"image/jpeg"` - `"image/png"` - `"image/gif"` - `"image/heic"` - `"image/heif"` - `"image/tiff"` - `"image/bmp"` - `"image/svg+xml"` - `"image/webp"` - `"image/x-icon"` - `"video/mp4"` - `"video/quicktime"` - `"video/mpeg"` - `"video/mpeg2"` - `"video/x-m4v"` - `"video/x-msvideo"` - `"video/3gpp"` - `"audio/mpeg"` - `"audio/mp3"` - `"audio/x-m4a"` - `"audio/mp4"` - `"audio/x-caf"` - `"audio/x-wav"` - `"audio/x-aiff"` - `"audio/aiff"` - `"audio/aac"` - `"audio/midi"` - `"audio/amr"` - `"application/pdf"` - `"text/plain"` - `"text/markdown"` - `"text/vcard"` - `"text/rtf"` - `"text/csv"` - `"text/html"` - `"text/calendar"` - `"application/msword"` - `"application/vnd.openxmlformats-officedocument.wordprocessingml.document"` - `"application/vnd.ms-excel"` - `"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"` - `"application/vnd.ms-powerpoint"` - `"application/vnd.openxmlformats-officedocument.presentationml.presentation"` - `"application/x-iwork-pages-sffpages"` - `"application/x-iwork-numbers-sffnumbers"` - `"application/x-iwork-keynote-sffkey"` - `"application/epub+zip"` - `"text/xml"` - `"application/json"` - `"application/zip"` - `"application/x-gzip"` - `created_at: string` When the attachment was created - `filename: string` Original filename of the attachment - `size_bytes: number` Size of the attachment in bytes - `status: "pending" or "complete" or "failed"` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `"pending"` - `"complete"` - `"failed"` - `download_url: optional string` URL to download the attachment ### Example ```http curl https://api.linqapp.com/api/partner/v3/attachments/$ATTACHMENT_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "filename": "photo.jpg", "content_type": "image/jpeg", "size_bytes": 1024000, "status": "complete", "download_url": "https://cdn.linqapp.com/attachments/550e8400-e29b-41d4-a716-446655440000/photo.jpg", "created_at": "2024-01-15T10:30:00Z" } ``` ## Delete an attachment **delete** `/v3/attachments/{attachmentId}` Permanently delete an attachment owned by the authenticated partner. ### Path Parameters - `attachmentId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/attachments/$ATTACHMENT_ID \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` ## Domain Types ### Supported Content Type - `SupportedContentType = "image/jpeg" or "image/png" or "image/gif" or 47 more` Supported MIME types for file attachments and media URLs. **Images:** image/jpeg, image/png, image/gif, image/heic, image/heif, image/tiff, image/bmp, image/svg+xml, image/webp, image/x-icon **Videos:** video/mp4, video/quicktime, video/mpeg, video/mpeg2, video/x-msvideo, video/3gpp **Audio:** audio/mpeg, audio/x-m4a, audio/x-caf, audio/x-wav, audio/x-aiff, audio/aac, audio/midi, audio/amr **Documents:** application/pdf, text/plain, text/markdown, text/vcard, text/rtf, text/csv, text/html, text/calendar, text/xml, application/json, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/x-iwork-pages-sffpages, application/x-iwork-numbers-sffnumbers, application/x-iwork-keynote-sffkey, application/epub+zip, application/zip, application/x-gzip **Transcoded on delivery:** - `audio/x-caf` — CAF files are transcoded to `audio/mp4` for delivery. **Deprecated (accepted but transcoded):** - `audio/mp3` — Deprecated. Use `audio/mpeg` instead. Files sent as audio/mp3 will be delivered as audio/mpeg. - `audio/mp4` — Deprecated. Use `audio/x-m4a` instead. Files sent as audio/mp4 will be delivered as audio/x-m4a. - `audio/aiff` — Deprecated. Use `audio/x-aiff` instead. Files sent as audio/aiff will be delivered as audio/x-aiff. - `image/tiff` — Accepted, but TIFF images are transcoded to JPEG for delivery. **Unsupported:** FLAC, OGG, and executable files are explicitly rejected. - `"image/jpeg"` - `"image/png"` - `"image/gif"` - `"image/heic"` - `"image/heif"` - `"image/tiff"` - `"image/bmp"` - `"image/svg+xml"` - `"image/webp"` - `"image/x-icon"` - `"video/mp4"` - `"video/quicktime"` - `"video/mpeg"` - `"video/mpeg2"` - `"video/x-m4v"` - `"video/x-msvideo"` - `"video/3gpp"` - `"audio/mpeg"` - `"audio/mp3"` - `"audio/x-m4a"` - `"audio/mp4"` - `"audio/x-caf"` - `"audio/x-wav"` - `"audio/x-aiff"` - `"audio/aiff"` - `"audio/aac"` - `"audio/midi"` - `"audio/amr"` - `"application/pdf"` - `"text/plain"` - `"text/markdown"` - `"text/vcard"` - `"text/rtf"` - `"text/csv"` - `"text/html"` - `"text/calendar"` - `"application/msword"` - `"application/vnd.openxmlformats-officedocument.wordprocessingml.document"` - `"application/vnd.ms-excel"` - `"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"` - `"application/vnd.ms-powerpoint"` - `"application/vnd.openxmlformats-officedocument.presentationml.presentation"` - `"application/x-iwork-pages-sffpages"` - `"application/x-iwork-numbers-sffnumbers"` - `"application/x-iwork-keynote-sffkey"` - `"application/epub+zip"` - `"text/xml"` - `"application/json"` - `"application/zip"` - `"application/x-gzip"` ### Attachment Create Response - `AttachmentCreateResponse object { attachment_id, download_url, expires_at, 3 more }` - `attachment_id: string` Unique identifier for the attachment - `download_url: string` Permanent CDN URL for the file. Does not expire. Use the `attachment_id` to reference this file in media parts when sending messages. - `expires_at: string` When the upload URL expires (15 minutes from now) - `http_method: "PUT"` HTTP method to use for upload (always PUT) - `"PUT"` - `required_headers: map[string]` HTTP headers that must be set on the upload request. The presigned URL is signed with these exact values — S3 will reject the upload if they don't match. - `upload_url: string` Presigned URL for uploading the file. PUT the raw binary file content to this URL with the `required_headers`. Do not JSON-encode or multipart-wrap the body. Expires after 15 minutes. ### Attachment Retrieve Response - `AttachmentRetrieveResponse object { id, content_type, created_at, 4 more }` - `id: string` Unique identifier for the attachment (UUID) - `content_type: SupportedContentType` Supported MIME types for file attachments and media URLs. **Images:** image/jpeg, image/png, image/gif, image/heic, image/heif, image/tiff, image/bmp, image/svg+xml, image/webp, image/x-icon **Videos:** video/mp4, video/quicktime, video/mpeg, video/mpeg2, video/x-msvideo, video/3gpp **Audio:** audio/mpeg, audio/x-m4a, audio/x-caf, audio/x-wav, audio/x-aiff, audio/aac, audio/midi, audio/amr **Documents:** application/pdf, text/plain, text/markdown, text/vcard, text/rtf, text/csv, text/html, text/calendar, text/xml, application/json, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/x-iwork-pages-sffpages, application/x-iwork-numbers-sffnumbers, application/x-iwork-keynote-sffkey, application/epub+zip, application/zip, application/x-gzip **Transcoded on delivery:** - `audio/x-caf` — CAF files are transcoded to `audio/mp4` for delivery. **Deprecated (accepted but transcoded):** - `audio/mp3` — Deprecated. Use `audio/mpeg` instead. Files sent as audio/mp3 will be delivered as audio/mpeg. - `audio/mp4` — Deprecated. Use `audio/x-m4a` instead. Files sent as audio/mp4 will be delivered as audio/x-m4a. - `audio/aiff` — Deprecated. Use `audio/x-aiff` instead. Files sent as audio/aiff will be delivered as audio/x-aiff. - `image/tiff` — Accepted, but TIFF images are transcoded to JPEG for delivery. **Unsupported:** FLAC, OGG, and executable files are explicitly rejected. - `"image/jpeg"` - `"image/png"` - `"image/gif"` - `"image/heic"` - `"image/heif"` - `"image/tiff"` - `"image/bmp"` - `"image/svg+xml"` - `"image/webp"` - `"image/x-icon"` - `"video/mp4"` - `"video/quicktime"` - `"video/mpeg"` - `"video/mpeg2"` - `"video/x-m4v"` - `"video/x-msvideo"` - `"video/3gpp"` - `"audio/mpeg"` - `"audio/mp3"` - `"audio/x-m4a"` - `"audio/mp4"` - `"audio/x-caf"` - `"audio/x-wav"` - `"audio/x-aiff"` - `"audio/aiff"` - `"audio/aac"` - `"audio/midi"` - `"audio/amr"` - `"application/pdf"` - `"text/plain"` - `"text/markdown"` - `"text/vcard"` - `"text/rtf"` - `"text/csv"` - `"text/html"` - `"text/calendar"` - `"application/msword"` - `"application/vnd.openxmlformats-officedocument.wordprocessingml.document"` - `"application/vnd.ms-excel"` - `"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"` - `"application/vnd.ms-powerpoint"` - `"application/vnd.openxmlformats-officedocument.presentationml.presentation"` - `"application/x-iwork-pages-sffpages"` - `"application/x-iwork-numbers-sffnumbers"` - `"application/x-iwork-keynote-sffkey"` - `"application/epub+zip"` - `"text/xml"` - `"application/json"` - `"application/zip"` - `"application/x-gzip"` - `created_at: string` When the attachment was created - `filename: string` Original filename of the attachment - `size_bytes: number` Size of the attachment in bytes - `status: "pending" or "complete" or "failed"` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `"pending"` - `"complete"` - `"failed"` - `download_url: optional string` URL to download the attachment --- # api/resources/attachments/methods/create/index.md URL: https://docs.linqapp.com/api/resources/attachments/methods/create/ ## Pre-upload a file **post** `/v3/attachments` **This endpoint is optional.** You can send media by simply providing a URL in your message's media part — no pre-upload required. Use this endpoint only when you want to upload a file ahead of time for reuse or latency optimization. Returns a presigned upload URL and a permanent `attachment_id` you can reference in future messages. ## Step 1: Request an upload URL Call this endpoint with file metadata: ```json POST /v3/attachments { "filename": "photo.jpg", "content_type": "image/jpeg", "size_bytes": 1024000 } ``` The response includes an `upload_url` (valid for 15 minutes) and a permanent `attachment_id`. ## Step 2: Upload the file Make a PUT request to the `upload_url` with the raw file bytes as the request body. You **must** include all headers from `required_headers` exactly as returned — the presigned URL is signed with these values and S3 will reject the upload if they don't match. The request body is the binary file content — **not** JSON, **not** multipart form data. The file must equal `size_bytes` bytes (the value you declared in step 1). ```bash curl -X PUT "<upload_url from step 1>" \ -H "Content-Type: image/jpeg" \ -H "Content-Length: 1024000" \ --data-binary @photo.jpg ``` ## Step 3: Send a message with the attachment Reference the `attachment_id` in a media part. The ID never expires — use it in as many messages as you want. ```json POST /v3/chats { "from": "+15559876543", "to": ["+15551234567"], "message": { "parts": [ { "type": "media", "attachment_id": "<attachment_id from step 1>" } ] } } ``` ## When to use this instead of a URL in the media part - Sending the same file to multiple recipients (avoids re-downloading each time) - Large files where you want to separate upload from message send - Latency-sensitive sends where the file should already be stored If you just need to send a file once, skip all of this and pass a `url` directly in the media part instead. **File Size Limit:** 100MB **Unsupported Types:** WebP, SVG, FLAC, OGG, and executable files are explicitly rejected. ### Body Parameters - `content_type: SupportedContentType` Supported MIME types for file attachments and media URLs. **Images:** image/jpeg, image/png, image/gif, image/heic, image/heif, image/tiff, image/bmp, image/svg+xml, image/webp, image/x-icon **Videos:** video/mp4, video/quicktime, video/mpeg, video/mpeg2, video/x-msvideo, video/3gpp **Audio:** audio/mpeg, audio/x-m4a, audio/x-caf, audio/x-wav, audio/x-aiff, audio/aac, audio/midi, audio/amr **Documents:** application/pdf, text/plain, text/markdown, text/vcard, text/rtf, text/csv, text/html, text/calendar, text/xml, application/json, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/x-iwork-pages-sffpages, application/x-iwork-numbers-sffnumbers, application/x-iwork-keynote-sffkey, application/epub+zip, application/zip, application/x-gzip **Transcoded on delivery:** - `audio/x-caf` — CAF files are transcoded to `audio/mp4` for delivery. **Deprecated (accepted but transcoded):** - `audio/mp3` — Deprecated. Use `audio/mpeg` instead. Files sent as audio/mp3 will be delivered as audio/mpeg. - `audio/mp4` — Deprecated. Use `audio/x-m4a` instead. Files sent as audio/mp4 will be delivered as audio/x-m4a. - `audio/aiff` — Deprecated. Use `audio/x-aiff` instead. Files sent as audio/aiff will be delivered as audio/x-aiff. - `image/tiff` — Accepted, but TIFF images are transcoded to JPEG for delivery. **Unsupported:** FLAC, OGG, and executable files are explicitly rejected. - `"image/jpeg"` - `"image/png"` - `"image/gif"` - `"image/heic"` - `"image/heif"` - `"image/tiff"` - `"image/bmp"` - `"image/svg+xml"` - `"image/webp"` - `"image/x-icon"` - `"video/mp4"` - `"video/quicktime"` - `"video/mpeg"` - `"video/mpeg2"` - `"video/x-m4v"` - `"video/x-msvideo"` - `"video/3gpp"` - `"audio/mpeg"` - `"audio/mp3"` - `"audio/x-m4a"` - `"audio/mp4"` - `"audio/x-caf"` - `"audio/x-wav"` - `"audio/x-aiff"` - `"audio/aiff"` - `"audio/aac"` - `"audio/midi"` - `"audio/amr"` - `"application/pdf"` - `"text/plain"` - `"text/markdown"` - `"text/vcard"` - `"text/rtf"` - `"text/csv"` - `"text/html"` - `"text/calendar"` - `"application/msword"` - `"application/vnd.openxmlformats-officedocument.wordprocessingml.document"` - `"application/vnd.ms-excel"` - `"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"` - `"application/vnd.ms-powerpoint"` - `"application/vnd.openxmlformats-officedocument.presentationml.presentation"` - `"application/x-iwork-pages-sffpages"` - `"application/x-iwork-numbers-sffnumbers"` - `"application/x-iwork-keynote-sffkey"` - `"application/epub+zip"` - `"text/xml"` - `"application/json"` - `"application/zip"` - `"application/x-gzip"` - `filename: string` Name of the file to upload - `size_bytes: number` Size of the file in bytes (max 100MB) ### Returns - `attachment_id: string` Unique identifier for the attachment - `download_url: string` Permanent CDN URL for the file. Does not expire. Use the `attachment_id` to reference this file in media parts when sending messages. - `expires_at: string` When the upload URL expires (15 minutes from now) - `http_method: "PUT"` HTTP method to use for upload (always PUT) - `"PUT"` - `required_headers: map[string]` HTTP headers that must be set on the upload request. The presigned URL is signed with these exact values — S3 will reject the upload if they don't match. - `upload_url: string` Presigned URL for uploading the file. PUT the raw binary file content to this URL with the `required_headers`. Do not JSON-encode or multipart-wrap the body. Expires after 15 minutes. ### Example ```http curl https://api.linqapp.com/api/partner/v3/attachments \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "content_type": "image/jpeg", "filename": "photo.jpg", "size_bytes": 1024000 }' ``` #### Response ```json { "attachment_id": "550e8400-e29b-41d4-a716-446655440000", "upload_url": "https://uploads.linqapp.com/attachments/550e8400?X-Amz-Algorithm=AWS4-HMAC-SHA256&...", "download_url": "https://cdn.linqapp.com/uploads/partner-id/550e8400/photo.jpg", "http_method": "PUT", "expires_at": "2024-01-15T10:45:00Z", "required_headers": { "Content-Type": "image/jpeg", "Content-Length": "1024000" } } ``` --- # api/resources/attachments/methods/delete/index.md URL: https://docs.linqapp.com/api/resources/attachments/methods/delete/ ## Delete an attachment **delete** `/v3/attachments/{attachmentId}` Permanently delete an attachment owned by the authenticated partner. ### Path Parameters - `attachmentId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/attachments/$ATTACHMENT_ID \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` --- # api/resources/attachments/methods/retrieve/index.md URL: https://docs.linqapp.com/api/resources/attachments/methods/retrieve/ ## Get attachment metadata **get** `/v3/attachments/{attachmentId}` Retrieve metadata for a specific attachment including file information, and URLs for downloading. `status`: (**deprecated** — will be removed in a future API version) ### Path Parameters - `attachmentId: string` ### Returns - `id: string` Unique identifier for the attachment (UUID) - `content_type: SupportedContentType` Supported MIME types for file attachments and media URLs. **Images:** image/jpeg, image/png, image/gif, image/heic, image/heif, image/tiff, image/bmp, image/svg+xml, image/webp, image/x-icon **Videos:** video/mp4, video/quicktime, video/mpeg, video/mpeg2, video/x-msvideo, video/3gpp **Audio:** audio/mpeg, audio/x-m4a, audio/x-caf, audio/x-wav, audio/x-aiff, audio/aac, audio/midi, audio/amr **Documents:** application/pdf, text/plain, text/markdown, text/vcard, text/rtf, text/csv, text/html, text/calendar, text/xml, application/json, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/x-iwork-pages-sffpages, application/x-iwork-numbers-sffnumbers, application/x-iwork-keynote-sffkey, application/epub+zip, application/zip, application/x-gzip **Transcoded on delivery:** - `audio/x-caf` — CAF files are transcoded to `audio/mp4` for delivery. **Deprecated (accepted but transcoded):** - `audio/mp3` — Deprecated. Use `audio/mpeg` instead. Files sent as audio/mp3 will be delivered as audio/mpeg. - `audio/mp4` — Deprecated. Use `audio/x-m4a` instead. Files sent as audio/mp4 will be delivered as audio/x-m4a. - `audio/aiff` — Deprecated. Use `audio/x-aiff` instead. Files sent as audio/aiff will be delivered as audio/x-aiff. - `image/tiff` — Accepted, but TIFF images are transcoded to JPEG for delivery. **Unsupported:** FLAC, OGG, and executable files are explicitly rejected. - `"image/jpeg"` - `"image/png"` - `"image/gif"` - `"image/heic"` - `"image/heif"` - `"image/tiff"` - `"image/bmp"` - `"image/svg+xml"` - `"image/webp"` - `"image/x-icon"` - `"video/mp4"` - `"video/quicktime"` - `"video/mpeg"` - `"video/mpeg2"` - `"video/x-m4v"` - `"video/x-msvideo"` - `"video/3gpp"` - `"audio/mpeg"` - `"audio/mp3"` - `"audio/x-m4a"` - `"audio/mp4"` - `"audio/x-caf"` - `"audio/x-wav"` - `"audio/x-aiff"` - `"audio/aiff"` - `"audio/aac"` - `"audio/midi"` - `"audio/amr"` - `"application/pdf"` - `"text/plain"` - `"text/markdown"` - `"text/vcard"` - `"text/rtf"` - `"text/csv"` - `"text/html"` - `"text/calendar"` - `"application/msword"` - `"application/vnd.openxmlformats-officedocument.wordprocessingml.document"` - `"application/vnd.ms-excel"` - `"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"` - `"application/vnd.ms-powerpoint"` - `"application/vnd.openxmlformats-officedocument.presentationml.presentation"` - `"application/x-iwork-pages-sffpages"` - `"application/x-iwork-numbers-sffnumbers"` - `"application/x-iwork-keynote-sffkey"` - `"application/epub+zip"` - `"text/xml"` - `"application/json"` - `"application/zip"` - `"application/x-gzip"` - `created_at: string` When the attachment was created - `filename: string` Original filename of the attachment - `size_bytes: number` Size of the attachment in bytes - `status: "pending" or "complete" or "failed"` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `"pending"` - `"complete"` - `"failed"` - `download_url: optional string` URL to download the attachment ### Example ```http curl https://api.linqapp.com/api/partner/v3/attachments/$ATTACHMENT_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "filename": "photo.jpg", "content_type": "image/jpeg", "size_bytes": 1024000, "status": "complete", "download_url": "https://cdn.linqapp.com/attachments/550e8400-e29b-41d4-a716-446655440000/photo.jpg", "created_at": "2024-01-15T10:30:00Z" } ``` --- # Available Number URL: https://docs.linqapp.com/api/resources/available_number/ ## Get an available sending number **get** `/v3/available_number` Returns the best available line (E.164) to send from, applying smart number assignment. Optionally pass `to` recipients to make the choice "sticky" — reusing the line an existing chat with those recipients is already on. Without `to`, the best healthy line is chosen. This is advisory: it does not reserve the line or change selection state. Pass the returned `phone_number` as `from` when you create the chat to guarantee the same line. Also returns `vcf_url`: a time-limited link to a vCard (`.vcf`) for the chosen line, carrying its contact card (name/photo) with the chosen number as the primary `TEL` and the partner's other healthy lines as backups. Share it with recipients so they can save the line as a contact. ### Query Parameters - `to: optional array of string` Recipient handles (E.164 or email) the message is destined for. When provided, an existing chat with these recipients makes the choice sticky. Repeat the parameter for multiple recipients. ### Returns - `phone_number: string` The selected sending line in E.164 format. - `vcf_url: string` Time-limited link to a vCard (`.vcf`) for the selected line. The card carries the line's contact details with the selected number as the primary `TEL` and the partner's other healthy lines as backups. The link expires; re-call this endpoint to mint a fresh one. ### Example ```http curl https://api.linqapp.com/api/partner/v3/available_number \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "phone_number": "+12025551234", "vcf_url": "https://s3.us-east-1.amazonaws.com/linq-attachments/vcf/9716d5c5/12025551234.vcf?X-Amz-Signature=..." } ``` ## Domain Types ### Available Number Retrieve Response - `AvailableNumberRetrieveResponse object { phone_number, vcf_url }` The line smart number assignment selected, plus a shareable vCard. - `phone_number: string` The selected sending line in E.164 format. - `vcf_url: string` Time-limited link to a vCard (`.vcf`) for the selected line. The card carries the line's contact details with the selected number as the primary `TEL` and the partner's other healthy lines as backups. The link expires; re-call this endpoint to mint a fresh one. --- # api/resources/available_number/methods/retrieve/index.md URL: https://docs.linqapp.com/api/resources/available_number/methods/retrieve/ ## Get an available sending number **get** `/v3/available_number` Returns the best available line (E.164) to send from, applying smart number assignment. Optionally pass `to` recipients to make the choice "sticky" — reusing the line an existing chat with those recipients is already on. Without `to`, the best healthy line is chosen. This is advisory: it does not reserve the line or change selection state. Pass the returned `phone_number` as `from` when you create the chat to guarantee the same line. Also returns `vcf_url`: a time-limited link to a vCard (`.vcf`) for the chosen line, carrying its contact card (name/photo) with the chosen number as the primary `TEL` and the partner's other healthy lines as backups. Share it with recipients so they can save the line as a contact. ### Query Parameters - `to: optional array of string` Recipient handles (E.164 or email) the message is destined for. When provided, an existing chat with these recipients makes the choice sticky. Repeat the parameter for multiple recipients. ### Returns - `phone_number: string` The selected sending line in E.164 format. - `vcf_url: string` Time-limited link to a vCard (`.vcf`) for the selected line. The card carries the line's contact details with the selected number as the primary `TEL` and the partner's other healthy lines as backups. The link expires; re-call this endpoint to mint a fresh one. ### Example ```http curl https://api.linqapp.com/api/partner/v3/available_number \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "phone_number": "+12025551234", "vcf_url": "https://s3.us-east-1.amazonaws.com/linq-attachments/vcf/9716d5c5/12025551234.vcf?X-Amz-Signature=..." } ``` --- # Capability URL: https://docs.linqapp.com/api/resources/capability/ ## Check iMessage capability **post** `/v3/capability/check_imessage` Check whether a recipient address (phone number or email) is reachable via iMessage. ### Body Parameters - `address: string` The recipient phone number or email address to check - `from: optional string` Optional sender phone number. If omitted, an available phone from your pool is used automatically. ### Returns - `HandleCheckResponse object { address, available }` - `address: string` The recipient address that was checked - `available: boolean` Whether the recipient supports the checked messaging service ### Example ```http curl https://api.linqapp.com/api/partner/v3/capability/check_imessage \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "address": "+15551234567", "from": "+15559876543" }' ``` #### Response ```json { "address": "+15551234567", "available": true } ``` ## Check RCS capability **post** `/v3/capability/check_rcs` Check whether a recipient address (phone number) supports RCS messaging. ### Body Parameters - `address: string` The recipient phone number or email address to check - `from: optional string` Optional sender phone number. If omitted, an available phone from your pool is used automatically. ### Returns - `HandleCheckResponse object { address, available }` - `address: string` The recipient address that was checked - `available: boolean` Whether the recipient supports the checked messaging service ### Example ```http curl https://api.linqapp.com/api/partner/v3/capability/check_rcs \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "address": "+15551234567", "from": "+15559876543" }' ``` #### Response ```json { "address": "+15551234567", "available": true } ``` ## Domain Types ### Handle Check - `HandleCheck object { address, from }` - `address: string` The recipient phone number or email address to check - `from: optional string` Optional sender phone number. If omitted, an available phone from your pool is used automatically. ### Handle Check Response - `HandleCheckResponse object { address, available }` - `address: string` The recipient address that was checked - `available: boolean` Whether the recipient supports the checked messaging service --- # api/resources/capability/methods/check_imessage/index.md URL: https://docs.linqapp.com/api/resources/capability/methods/check_imessage/ ## Check iMessage capability **post** `/v3/capability/check_imessage` Check whether a recipient address (phone number or email) is reachable via iMessage. ### Body Parameters - `address: string` The recipient phone number or email address to check - `from: optional string` Optional sender phone number. If omitted, an available phone from your pool is used automatically. ### Returns - `HandleCheckResponse object { address, available }` - `address: string` The recipient address that was checked - `available: boolean` Whether the recipient supports the checked messaging service ### Example ```http curl https://api.linqapp.com/api/partner/v3/capability/check_imessage \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "address": "+15551234567", "from": "+15559876543" }' ``` #### Response ```json { "address": "+15551234567", "available": true } ``` --- # api/resources/capability/methods/check_rcs/index.md URL: https://docs.linqapp.com/api/resources/capability/methods/check_rcs/ ## Check RCS capability **post** `/v3/capability/check_rcs` Check whether a recipient address (phone number) supports RCS messaging. ### Body Parameters - `address: string` The recipient phone number or email address to check - `from: optional string` Optional sender phone number. If omitted, an available phone from your pool is used automatically. ### Returns - `HandleCheckResponse object { address, available }` - `address: string` The recipient address that was checked - `available: boolean` Whether the recipient supports the checked messaging service ### Example ```http curl https://api.linqapp.com/api/partner/v3/capability/check_rcs \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "address": "+15551234567", "from": "+15559876543" }' ``` #### Response ```json { "address": "+15551234567", "available": true } ``` --- # Chats URL: https://docs.linqapp.com/api/resources/chats/ ## 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. ## Message Effects You can add iMessage effects to make your messages more expressive. Effects are optional and can be either screen effects (full-screen animations) or bubble effects (message bubble animations). **Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`, `hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight` **Bubble Effects:** `slam`, `loud`, `gentle`, `invisible` Only one effect type can be applied per message. ## Inline Text Decorations (iMessage only) Use the `text_decorations` array on a text part to apply styling and animations to character ranges. Each decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`. **Styles:** `bold`, `italic`, `strikethrough`, `underline` **Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter` ```json { "type": "text", "value": "Hello world", "text_decorations": [ { "range": [0, 5], "style": "bold" }, { "range": [6, 11], "animation": "shake" } ] } ``` **Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. ## First-Message Link Restriction 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. ### Body Parameters - `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` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. - `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. ### Returns - `chat: object { id, display_name, handles, 4 more }` - `id: string` Unique identifier for the created chat (UUID) - `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` 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 - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: boolean` Whether this is a group chat - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `is_me: optional boolean` Whether this handle belongs to the sender (your phone number) - `left_at: optional string` When they left (if applicable) - `status: optional "active" or "left" or "removed"` Participant status - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type - `service: ServiceType` Messaging service type ### Example ```http 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" ] }' ``` #### Response ```json { "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": "iMessage", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "health_status": { "doc_url": "https://docs.linqapp.com/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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" }, "service": "iMessage" } } ``` ## List all chats **get** `/v3/chats` Retrieves a paginated list of chats for the authenticated partner. **Filtering:** - If `from` is provided, returns chats for that specific phone number - If `from` is omitted, returns chats across all phone numbers owned by the partner - If `to` is provided, only returns chats where the specified handle is a participant **Pagination:** - Use `limit` to control page size (default: 20, max: 100) - The response includes `next_cursor` for fetching the next page - When `next_cursor` is `null`, there are no more results to fetch - Pass the `next_cursor` value as the `cursor` parameter for the next request **Example pagination flow:** 1. First request: `GET /v3/chats?from=%2B12223334444&limit=20` 1. Response includes `next_cursor: "20"` (more results exist) 1. Next request: `GET /v3/chats?from=%2B12223334444&limit=20&cursor=20` 1. Response includes `next_cursor: null` (no more results) ### Query Parameters - `cursor: optional string` Pagination cursor from the previous response's `next_cursor` field. Omit this parameter for the first page of results. - `from: optional string` Phone number to filter chats by. Returns chats made from this phone number. Must be in E.164 format (e.g., `+13343284472`). The `+` is automatically URL-encoded by HTTP clients. If omitted, returns chats across all phone numbers owned by the partner. - `limit: optional number` Maximum number of chats to return per page - `to: optional string` Filter chats by a participant handle. Only returns chats where this handle is a participant. Can be an E.164 phone number (e.g., `+13343284472`) or an email address (e.g., `user@example.com`). For phone numbers, the `+` is automatically URL-encoded by HTTP clients. ### Returns - `chats: array of Chat` List of chats - `id: string` Unique identifier for the chat - `created_at: string` When the chat was created - `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` List of chat participants with full handle details. Always contains at least two handles (your phone number and the other participant). - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_archived: boolean` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `is_group: boolean` Whether this is a group chat - `updated_at: string` When the chat was last updated - `group_chat_icon: optional string` URL of the group chat icon. Only set for group chats that have an icon; `null` otherwise. - `service: optional ServiceType` Messaging service type - `next_cursor: optional string` Cursor for fetching the next page of results. Null if there are no more results to fetch. Pass this value as the `cursor` parameter in the next request. ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "chats": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "display_name": "+14155551234, +14155559876", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440010", "handle": "+14155551234", "joined_at": "2025-05-21T15:30:00.000Z", "service": "iMessage", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "health_status": { "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#at-risk", "status": "AT_RISK", "updated_at": "2026-05-01T18:28:25Z" }, "is_archived": true, "is_group": true, "updated_at": "2024-01-15T10:30:00Z", "group_chat_icon": "https://example.com/group-icon.png", "service": "iMessage" } ], "next_cursor": "next_cursor" } ``` ## Get a chat by ID **get** `/v3/chats/{chatId}` Retrieve a chat by its unique identifier. ### Path Parameters - `chatId: string` ### Returns - `Chat object { id, created_at, display_name, 7 more }` - `id: string` Unique identifier for the chat - `created_at: string` When the chat was created - `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` List of chat participants with full handle details. Always contains at least two handles (your phone number and the other participant). - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_archived: boolean` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `is_group: boolean` Whether this is a group chat - `updated_at: string` When the chat was last updated - `group_chat_icon: optional string` URL of the group chat icon. Only set for group chats that have an icon; `null` otherwise. - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "display_name": "+14155551234, +14155559876", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440010", "handle": "+14155551234", "joined_at": "2025-05-21T15:30:00.000Z", "service": "iMessage", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "health_status": { "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#at-risk", "status": "AT_RISK", "updated_at": "2026-05-01T18:28:25Z" }, "is_archived": true, "is_group": true, "updated_at": "2024-01-15T10:30:00Z", "group_chat_icon": "https://example.com/group-icon.png", "service": "iMessage" } ``` ## Update a chat **put** `/v3/chats/{chatId}` Update chat properties such as display name and group chat icon. Listen for `chat.group_name_updated`, `chat.group_icon_updated`, `chat.group_name_update_failed`, or `chat.group_icon_update_failed` webhook events to confirm the outcome. ### Path Parameters - `chatId: string` ### Body Parameters - `display_name: optional string` New display name for the chat (group chats only) - `group_chat_icon: optional string` URL of an image to set as the group chat icon (group chats only) ### Returns - `chat_id: optional string` - `status: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID \ -X PUT \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "display_name": "Team Discussion", "group_chat_icon": "https://example.com/icon.png" }' ``` #### Response ```json { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending" } ``` ## Mark chat as read **post** `/v3/chats/{chatId}/read` Mark all messages in a chat as read. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/read \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` ## Leave a group chat **post** `/v3/chats/{chatId}/leave` Removes your phone number from a group chat. Once you leave, you will no longer receive messages from the group and all interaction endpoints (send message, typing, mark read, etc.) will return 409. A `participant.removed` webhook will fire once the leave has been processed. **Supported** - iMessage group chats with 4 or more active participants (including yourself) **Not supported** - DM (1-on-1) chats — use the chat directly to continue the conversation ### Path Parameters - `chatId: string` ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/leave \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "message": "Leave group chat queued", "status": "accepted", "trace_id": "trace_id" } ``` ## Share your contact card with a chat **post** `/v3/chats/{chatId}/share_contact_card` Share your contact information (Name and Photo Sharing) with a chat. **Note:** A contact card must be configured before sharing. You can set up your contact card via the [Contact Card API](#tag/Contact-Card) or on the [Linq dashboard](https://dashboard.linqapp.com/contact-cards). ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/share_contact_card \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` ## Send a voice memo to a chat **post** `/v3/chats/{chatId}/voicememo` Send an audio file as an **iMessage voice memo bubble** to all participants in a chat. Voice memos appear with iMessage's native inline playback UI, unlike regular audio attachments sent via media parts which appear as downloadable files. **Supported audio formats:** - MP3 (audio/mpeg) - M4A (audio/x-m4a, audio/mp4) - AAC (audio/aac) - CAF (audio/x-caf) - Core Audio Format - WAV (audio/wav) - AIFF (audio/aiff, audio/x-aiff) - AMR (audio/amr) ### Path Parameters - `chatId: string` ### Body Parameters - `attachment_id: optional string` Reference to a voice memo file pre-uploaded via `POST /v3/attachments`. The file is already stored, so sends using this ID skip the download step. Either `voice_memo_url` or `attachment_id` must be provided, but not both. - `voice_memo_url: optional string` URL of the voice memo audio file. Must be a publicly accessible HTTPS URL. Either `voice_memo_url` or `attachment_id` must be provided, but not both. ### Returns - `voice_memo: object { id, chat, created_at, 5 more }` - `id: string` Message identifier - `chat: object { id, handles, is_active, 2 more }` - `id: string` Chat identifier - `handles: array of ChatHandle` Chat participants - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_active: boolean` Whether the chat is active - `is_group: boolean` Whether this is a group chat - `service: ServiceType` Messaging service type - `created_at: string` When the voice memo was created - `from: string` Sender phone number - `status: string` Current delivery status - `to: array of string` Recipient handles (phone numbers or email addresses) - `voice_memo: object { id, filename, mime_type, 3 more }` - `id: string` Attachment identifier - `filename: string` Original filename - `mime_type: string` Audio MIME type - `size_bytes: number` File size in bytes - `url: string` CDN URL for downloading the voice memo - `duration_ms: optional number` Duration in milliseconds - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/voicememo \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "voice_memo_url": "https://example.com/voice-memo.m4a" }' ``` #### Response ```json { "voice_memo": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat": { "id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "is_active": true, "is_group": true, "service": "iMessage" }, "created_at": "2019-12-27T18:11:19.117Z", "from": "+12052535597", "status": "queued", "to": [ "+12052532136" ], "voice_memo": { "id": "550e8400-e29b-41d4-a716-446655440000", "filename": "voice-memo.m4a", "mime_type": "audio/x-m4a", "size_bytes": 524288, "url": "https://cdn.linqapp.com/voice-memos/abc123.m4a", "duration_ms": 15000 }, "service": "iMessage" } } ``` ## Domain Types ### Chat - `Chat object { id, created_at, display_name, 7 more }` - `id: string` Unique identifier for the chat - `created_at: string` When the chat was created - `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` List of chat participants with full handle details. Always contains at least two handles (your phone number and the other participant). - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_archived: boolean` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `is_group: boolean` Whether this is a group chat - `updated_at: string` When the chat was last updated - `group_chat_icon: optional string` URL of the group chat icon. Only set for group chats that have an icon; `null` otherwise. - `service: optional ServiceType` Messaging service type ### Link Part - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. ### Media Part - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. ### Message Content - `MessageContent object { parts, effect, idempotency_key, 2 more }` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. ### Text Part - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` ### Chat Create Response - `ChatCreateResponse object { chat }` Response for creating a new chat with an initial message - `chat: object { id, display_name, handles, 4 more }` - `id: string` Unique identifier for the created chat (UUID) - `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` 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 - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: boolean` Whether this is a group chat - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `is_me: optional boolean` Whether this handle belongs to the sender (your phone number) - `left_at: optional string` When they left (if applicable) - `status: optional "active" or "left" or "removed"` Participant status - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type - `service: ServiceType` Messaging service type ### Chat Update Response - `ChatUpdateResponse object { chat_id, status }` - `chat_id: optional string` - `status: optional string` ### Chat Leave Chat Response - `ChatLeaveChatResponse object { message, status, trace_id }` - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Chat Send Voicememo Response - `ChatSendVoicememoResponse object { voice_memo }` Response for sending a voice memo to a chat - `voice_memo: object { id, chat, created_at, 5 more }` - `id: string` Message identifier - `chat: object { id, handles, is_active, 2 more }` - `id: string` Chat identifier - `handles: array of ChatHandle` Chat participants - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_active: boolean` Whether the chat is active - `is_group: boolean` Whether this is a group chat - `service: ServiceType` Messaging service type - `created_at: string` When the voice memo was created - `from: string` Sender phone number - `status: string` Current delivery status - `to: array of string` Recipient handles (phone numbers or email addresses) - `voice_memo: object { id, filename, mime_type, 3 more }` - `id: string` Attachment identifier - `filename: string` Original filename - `mime_type: string` Audio MIME type - `size_bytes: number` File size in bytes - `url: string` CDN URL for downloading the voice memo - `duration_ms: optional number` Duration in milliseconds - `service: optional ServiceType` Messaging service type # Participants ## Add a participant to a chat **post** `/v3/chats/{chatId}/participants` Add a new participant to an existing group chat. **Requirements:** - Group chats only (3+ existing participants) - New participant must support the same messaging service as the group - Cross-service additions not allowed (e.g., can't add RCS-only user to iMessage group) - For cross-service scenarios, create a new chat instead ### Path Parameters - `chatId: string` ### Body Parameters - `handle: string` Phone number (E.164 format) or email address of the participant to add ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/participants \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "handle": "+12052499136" }' ``` #### Response ```json { "message": "Participant addition queued", "status": "accepted", "trace_id": "trace_id" } ``` ## Remove a participant from a chat **delete** `/v3/chats/{chatId}/participants` Remove a participant from an existing group chat. **Requirements:** - Group chats only - Must have 3+ participants after removal ### Path Parameters - `chatId: string` ### Body Parameters - `handle: string` Phone number (E.164 format) or email address of the participant to remove ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/participants \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "message": "Participant removal queued", "status": "accepted", "trace_id": "trace_id" } ``` ## Domain Types ### Participant Add Response - `ParticipantAddResponse object { message, status, trace_id }` - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Participant Remove Response - `ParticipantRemoveResponse object { message, status, trace_id }` - `message: optional string` - `status: optional string` - `trace_id: optional string` # Typing ## Start typing indicator **post** `/v3/chats/{chatId}/typing` Send a typing indicator to show that someone is typing in the chat. ## Behavior & Limitations Typing indicators are best-effort signals with the following limitations: - **Active conversations only:** The recipient must have sent or received a message in this chat within the **last 5 minutes**. If the chat is inactive, the request is still accepted (`204`) but the indicator will not reach the recipient's device. - **No delivery guarantee:** Even for active chats, a `204` response only indicates the request was accepted for processing. - **Group chats not supported:** Attempting to start a typing indicator in a group chat will return a `403` error. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/typing \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` ## Stop typing indicator **delete** `/v3/chats/{chatId}/typing` Stop the typing indicator for the chat. Typing indicators are automatically stopped when a message is sent, so calling this endpoint after sending a message is unnecessary. See the `POST` endpoint above for behavior details and limitations. **Note:** Group chats are not supported and will return a `403` error. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/typing \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` # Messages ## Send a message to an existing chat **post** `/v3/chats/{chatId}/messages` Send a message to an existing chat. Use this endpoint when you already have a chat ID and want to send additional messages to it. ## Message Effects You can add iMessage effects to make your messages more expressive. Effects are optional and can be either screen effects (full-screen animations) or bubble effects (message bubble animations). **Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`, `hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight` **Bubble Effects:** `slam`, `loud`, `gentle`, `invisible` Only one effect type can be applied per message. ## Inline Text Decorations (iMessage only) Use the `text_decorations` array on a text part to apply styling and animations to character ranges. Each decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`. **Styles:** `bold`, `italic`, `strikethrough`, `underline` **Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter` ```json { "type": "text", "value": "Hello world", "text_decorations": [ { "range": [0, 5], "style": "bold" }, { "range": [6, 11], "animation": "shake" } ] } ``` **Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. ### Path Parameters - `chatId: string` ### Body Parameters - `message: MessageContent` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. ### Returns - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "message": { "parts": [ { "type": "text", "value": "Hello, world!" } ] } }' ``` #### Response ```json { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" } } ``` ## Get messages from a chat **get** `/v3/chats/{chatId}/messages` Retrieve messages from a specific chat with pagination support. ### Path Parameters - `chatId: string` ### Query Parameters - `cursor: optional string` Pagination cursor from previous next_cursor response - `limit: optional number` Maximum number of messages to return ### Returns - `messages: array of Message` List of messages - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type - `next_cursor: optional string` Cursor for fetching the next page of results. Null if there are no more results to fetch. Pass this value as the `cursor` parameter in the next request. ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "messages": [ { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ], "next_cursor": "next_cursor" } ``` ## Domain Types ### Sent Message - `SentMessage object { id, created_at, delivery_status, 9 more }` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type ### Message Send Response - `MessageSendResponse object { chat_id, message }` Response for sending a message to a chat - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type # Location ## Request location sharing **post** `/v3/chats/{chatId}/location/request` Send a location sharing request to a contact. They will receive an iMessage prompt asking them to share their location. Location requests only work in **1:1 iMessage chats** (Apple limitation). Attempting to request location in a group chat, or in an SMS or RCS chat, returns `409` (Operation not supported on this chat's service type). ### Path Parameters - `chatId: string` ### Returns - `LocationRequestResponse object { message, success }` - `message: string` - `success: boolean` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/location/request \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "success": true, "message": "Location request sent" } ``` ## Get location data **get** `/v3/chats/{chatId}/location` Retrieve the current location for contacts sharing with you in a chat. The response is wrapped in the standard `{ "success": true, "data": ... }` envelope — the body is **not** a bare GeoJSON document. `data` is a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) `FeatureCollection` with a `Feature` for each participant actively sharing their location. Works for both 1:1 and group chats. In group chats, `data.features` contains a separate feature for each participant who is sharing. Each feature's `properties.handle` identifies the user. Returns an empty `data.features` array if no one is sharing or no location data is available yet. ### Path Parameters - `chatId: string` ### Returns - `GetChatLocationResponse object { data, success }` - `data: object { features, type }` - `features: array of object { geometry, properties, type }` - `geometry: object { coordinates, type }` - `coordinates: array of number` [longitude, latitude] or [longitude, latitude, altitude] - `type: "Point"` - `"Point"` - `properties: object { handle, address, locality, updated_at }` - `handle: string` Phone number or email of the person sharing their location - `address: optional string` Full street address - `locality: optional string` City or locality name - `updated_at: optional string` When the location was last updated - `type: "Feature"` - `"Feature"` - `type: "FeatureCollection"` - `"FeatureCollection"` - `success: boolean` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/location \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` ## Domain Types ### Get Chat Location Response - `GetChatLocationResponse object { data, success }` - `data: object { features, type }` - `features: array of object { geometry, properties, type }` - `geometry: object { coordinates, type }` - `coordinates: array of number` [longitude, latitude] or [longitude, latitude, altitude] - `type: "Point"` - `"Point"` - `properties: object { handle, address, locality, updated_at }` - `handle: string` Phone number or email of the person sharing their location - `address: optional string` Full street address - `locality: optional string` City or locality name - `updated_at: optional string` When the location was last updated - `type: "Feature"` - `"Feature"` - `type: "FeatureCollection"` - `"FeatureCollection"` - `success: boolean` ### Location Request Response - `LocationRequestResponse object { message, success }` - `message: string` - `success: boolean` --- # api/resources/chats/methods/create/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/create/ ## 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. ## Message Effects You can add iMessage effects to make your messages more expressive. Effects are optional and can be either screen effects (full-screen animations) or bubble effects (message bubble animations). **Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`, `hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight` **Bubble Effects:** `slam`, `loud`, `gentle`, `invisible` Only one effect type can be applied per message. ## Inline Text Decorations (iMessage only) Use the `text_decorations` array on a text part to apply styling and animations to character ranges. Each decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`. **Styles:** `bold`, `italic`, `strikethrough`, `underline` **Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter` ```json { "type": "text", "value": "Hello world", "text_decorations": [ { "range": [0, 5], "style": "bold" }, { "range": [6, 11], "animation": "shake" } ] } ``` **Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. ## First-Message Link Restriction 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. ### Body Parameters - `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` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. - `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. ### Returns - `chat: object { id, display_name, handles, 4 more }` - `id: string` Unique identifier for the created chat (UUID) - `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` 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 - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: boolean` Whether this is a group chat - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `is_me: optional boolean` Whether this handle belongs to the sender (your phone number) - `left_at: optional string` When they left (if applicable) - `status: optional "active" or "left" or "removed"` Participant status - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type - `service: ServiceType` Messaging service type ### Example ```http 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" ] }' ``` #### Response ```json { "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": "iMessage", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "health_status": { "doc_url": "https://docs.linqapp.com/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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" }, "service": "iMessage" } } ``` --- # api/resources/chats/methods/leave_chat/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/leave_chat/ ## Leave a group chat **post** `/v3/chats/{chatId}/leave` Removes your phone number from a group chat. Once you leave, you will no longer receive messages from the group and all interaction endpoints (send message, typing, mark read, etc.) will return 409. A `participant.removed` webhook will fire once the leave has been processed. **Supported** - iMessage group chats with 4 or more active participants (including yourself) **Not supported** - DM (1-on-1) chats — use the chat directly to continue the conversation ### Path Parameters - `chatId: string` ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/leave \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "message": "Leave group chat queued", "status": "accepted", "trace_id": "trace_id" } ``` --- # api/resources/chats/methods/list_chats/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/list_chats/ ## List all chats **get** `/v3/chats` Retrieves a paginated list of chats for the authenticated partner. **Filtering:** - If `from` is provided, returns chats for that specific phone number - If `from` is omitted, returns chats across all phone numbers owned by the partner - If `to` is provided, only returns chats where the specified handle is a participant **Pagination:** - Use `limit` to control page size (default: 20, max: 100) - The response includes `next_cursor` for fetching the next page - When `next_cursor` is `null`, there are no more results to fetch - Pass the `next_cursor` value as the `cursor` parameter for the next request **Example pagination flow:** 1. First request: `GET /v3/chats?from=%2B12223334444&limit=20` 1. Response includes `next_cursor: "20"` (more results exist) 1. Next request: `GET /v3/chats?from=%2B12223334444&limit=20&cursor=20` 1. Response includes `next_cursor: null` (no more results) ### Query Parameters - `cursor: optional string` Pagination cursor from the previous response's `next_cursor` field. Omit this parameter for the first page of results. - `from: optional string` Phone number to filter chats by. Returns chats made from this phone number. Must be in E.164 format (e.g., `+13343284472`). The `+` is automatically URL-encoded by HTTP clients. If omitted, returns chats across all phone numbers owned by the partner. - `limit: optional number` Maximum number of chats to return per page - `to: optional string` Filter chats by a participant handle. Only returns chats where this handle is a participant. Can be an E.164 phone number (e.g., `+13343284472`) or an email address (e.g., `user@example.com`). For phone numbers, the `+` is automatically URL-encoded by HTTP clients. ### Returns - `chats: array of Chat` List of chats - `id: string` Unique identifier for the chat - `created_at: string` When the chat was created - `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` List of chat participants with full handle details. Always contains at least two handles (your phone number and the other participant). - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_archived: boolean` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `is_group: boolean` Whether this is a group chat - `updated_at: string` When the chat was last updated - `group_chat_icon: optional string` URL of the group chat icon. Only set for group chats that have an icon; `null` otherwise. - `service: optional ServiceType` Messaging service type - `next_cursor: optional string` Cursor for fetching the next page of results. Null if there are no more results to fetch. Pass this value as the `cursor` parameter in the next request. ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "chats": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "display_name": "+14155551234, +14155559876", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440010", "handle": "+14155551234", "joined_at": "2025-05-21T15:30:00.000Z", "service": "iMessage", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "health_status": { "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#at-risk", "status": "AT_RISK", "updated_at": "2026-05-01T18:28:25Z" }, "is_archived": true, "is_group": true, "updated_at": "2024-01-15T10:30:00Z", "group_chat_icon": "https://example.com/group-icon.png", "service": "iMessage" } ], "next_cursor": "next_cursor" } ``` --- # api/resources/chats/methods/mark_as_read/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/mark_as_read/ ## Mark chat as read **post** `/v3/chats/{chatId}/read` Mark all messages in a chat as read. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/read \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` --- # api/resources/chats/methods/retrieve/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/retrieve/ ## Get a chat by ID **get** `/v3/chats/{chatId}` Retrieve a chat by its unique identifier. ### Path Parameters - `chatId: string` ### Returns - `Chat object { id, created_at, display_name, 7 more }` - `id: string` Unique identifier for the chat - `created_at: string` When the chat was created - `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` List of chat participants with full handle details. Always contains at least two handles (your phone number and the other participant). - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_archived: boolean` **DEPRECATED:** This field is deprecated and will be removed in a future API version. - `is_group: boolean` Whether this is a group chat - `updated_at: string` When the chat was last updated - `group_chat_icon: optional string` URL of the group chat icon. Only set for group chats that have an icon; `null` otherwise. - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "display_name": "+14155551234, +14155559876", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440010", "handle": "+14155551234", "joined_at": "2025-05-21T15:30:00.000Z", "service": "iMessage", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "health_status": { "doc_url": "https://docs.linqapp.com/guides/chats/chat-health#at-risk", "status": "AT_RISK", "updated_at": "2026-05-01T18:28:25Z" }, "is_archived": true, "is_group": true, "updated_at": "2024-01-15T10:30:00Z", "group_chat_icon": "https://example.com/group-icon.png", "service": "iMessage" } ``` --- # api/resources/chats/methods/send_voicememo/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/send_voicememo/ ## Send a voice memo to a chat **post** `/v3/chats/{chatId}/voicememo` Send an audio file as an **iMessage voice memo bubble** to all participants in a chat. Voice memos appear with iMessage's native inline playback UI, unlike regular audio attachments sent via media parts which appear as downloadable files. **Supported audio formats:** - MP3 (audio/mpeg) - M4A (audio/x-m4a, audio/mp4) - AAC (audio/aac) - CAF (audio/x-caf) - Core Audio Format - WAV (audio/wav) - AIFF (audio/aiff, audio/x-aiff) - AMR (audio/amr) ### Path Parameters - `chatId: string` ### Body Parameters - `attachment_id: optional string` Reference to a voice memo file pre-uploaded via `POST /v3/attachments`. The file is already stored, so sends using this ID skip the download step. Either `voice_memo_url` or `attachment_id` must be provided, but not both. - `voice_memo_url: optional string` URL of the voice memo audio file. Must be a publicly accessible HTTPS URL. Either `voice_memo_url` or `attachment_id` must be provided, but not both. ### Returns - `voice_memo: object { id, chat, created_at, 5 more }` - `id: string` Message identifier - `chat: object { id, handles, is_active, 2 more }` - `id: string` Chat identifier - `handles: array of ChatHandle` Chat participants - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_active: boolean` Whether the chat is active - `is_group: boolean` Whether this is a group chat - `service: ServiceType` Messaging service type - `created_at: string` When the voice memo was created - `from: string` Sender phone number - `status: string` Current delivery status - `to: array of string` Recipient handles (phone numbers or email addresses) - `voice_memo: object { id, filename, mime_type, 3 more }` - `id: string` Attachment identifier - `filename: string` Original filename - `mime_type: string` Audio MIME type - `size_bytes: number` File size in bytes - `url: string` CDN URL for downloading the voice memo - `duration_ms: optional number` Duration in milliseconds - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/voicememo \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "voice_memo_url": "https://example.com/voice-memo.m4a" }' ``` #### Response ```json { "voice_memo": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat": { "id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "is_active": true, "is_group": true, "service": "iMessage" }, "created_at": "2019-12-27T18:11:19.117Z", "from": "+12052535597", "status": "queued", "to": [ "+12052532136" ], "voice_memo": { "id": "550e8400-e29b-41d4-a716-446655440000", "filename": "voice-memo.m4a", "mime_type": "audio/x-m4a", "size_bytes": 524288, "url": "https://cdn.linqapp.com/voice-memos/abc123.m4a", "duration_ms": 15000 }, "service": "iMessage" } } ``` --- # api/resources/chats/methods/share_contact_card/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/share_contact_card/ ## Share your contact card with a chat **post** `/v3/chats/{chatId}/share_contact_card` Share your contact information (Name and Photo Sharing) with a chat. **Note:** A contact card must be configured before sharing. You can set up your contact card via the [Contact Card API](#tag/Contact-Card) or on the [Linq dashboard](https://dashboard.linqapp.com/contact-cards). ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/share_contact_card \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` --- # api/resources/chats/methods/update/index.md URL: https://docs.linqapp.com/api/resources/chats/methods/update/ ## Update a chat **put** `/v3/chats/{chatId}` Update chat properties such as display name and group chat icon. Listen for `chat.group_name_updated`, `chat.group_icon_updated`, `chat.group_name_update_failed`, or `chat.group_icon_update_failed` webhook events to confirm the outcome. ### Path Parameters - `chatId: string` ### Body Parameters - `display_name: optional string` New display name for the chat (group chats only) - `group_chat_icon: optional string` URL of an image to set as the group chat icon (group chats only) ### Returns - `chat_id: optional string` - `status: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID \ -X PUT \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "display_name": "Team Discussion", "group_chat_icon": "https://example.com/icon.png" }' ``` #### Response ```json { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending" } ``` --- # Location URL: https://docs.linqapp.com/api/resources/chats/subresources/location/ ## Request location sharing **post** `/v3/chats/{chatId}/location/request` Send a location sharing request to a contact. They will receive an iMessage prompt asking them to share their location. Location requests only work in **1:1 iMessage chats** (Apple limitation). Attempting to request location in a group chat, or in an SMS or RCS chat, returns `409` (Operation not supported on this chat's service type). ### Path Parameters - `chatId: string` ### Returns - `LocationRequestResponse object { message, success }` - `message: string` - `success: boolean` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/location/request \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "success": true, "message": "Location request sent" } ``` ## Get location data **get** `/v3/chats/{chatId}/location` Retrieve the current location for contacts sharing with you in a chat. The response is wrapped in the standard `{ "success": true, "data": ... }` envelope — the body is **not** a bare GeoJSON document. `data` is a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) `FeatureCollection` with a `Feature` for each participant actively sharing their location. Works for both 1:1 and group chats. In group chats, `data.features` contains a separate feature for each participant who is sharing. Each feature's `properties.handle` identifies the user. Returns an empty `data.features` array if no one is sharing or no location data is available yet. ### Path Parameters - `chatId: string` ### Returns - `GetChatLocationResponse object { data, success }` - `data: object { features, type }` - `features: array of object { geometry, properties, type }` - `geometry: object { coordinates, type }` - `coordinates: array of number` [longitude, latitude] or [longitude, latitude, altitude] - `type: "Point"` - `"Point"` - `properties: object { handle, address, locality, updated_at }` - `handle: string` Phone number or email of the person sharing their location - `address: optional string` Full street address - `locality: optional string` City or locality name - `updated_at: optional string` When the location was last updated - `type: "Feature"` - `"Feature"` - `type: "FeatureCollection"` - `"FeatureCollection"` - `success: boolean` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/location \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` ## Domain Types ### Get Chat Location Response - `GetChatLocationResponse object { data, success }` - `data: object { features, type }` - `features: array of object { geometry, properties, type }` - `geometry: object { coordinates, type }` - `coordinates: array of number` [longitude, latitude] or [longitude, latitude, altitude] - `type: "Point"` - `"Point"` - `properties: object { handle, address, locality, updated_at }` - `handle: string` Phone number or email of the person sharing their location - `address: optional string` Full street address - `locality: optional string` City or locality name - `updated_at: optional string` When the location was last updated - `type: "Feature"` - `"Feature"` - `type: "FeatureCollection"` - `"FeatureCollection"` - `success: boolean` ### Location Request Response - `LocationRequestResponse object { message, success }` - `message: string` - `success: boolean` --- # api/resources/chats/subresources/location/methods/request/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/location/methods/request/ ## Request location sharing **post** `/v3/chats/{chatId}/location/request` Send a location sharing request to a contact. They will receive an iMessage prompt asking them to share their location. Location requests only work in **1:1 iMessage chats** (Apple limitation). Attempting to request location in a group chat, or in an SMS or RCS chat, returns `409` (Operation not supported on this chat's service type). ### Path Parameters - `chatId: string` ### Returns - `LocationRequestResponse object { message, success }` - `message: string` - `success: boolean` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/location/request \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "success": true, "message": "Location request sent" } ``` --- # api/resources/chats/subresources/location/methods/retrieve/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/location/methods/retrieve/ ## Get location data **get** `/v3/chats/{chatId}/location` Retrieve the current location for contacts sharing with you in a chat. The response is wrapped in the standard `{ "success": true, "data": ... }` envelope — the body is **not** a bare GeoJSON document. `data` is a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) `FeatureCollection` with a `Feature` for each participant actively sharing their location. Works for both 1:1 and group chats. In group chats, `data.features` contains a separate feature for each participant who is sharing. Each feature's `properties.handle` identifies the user. Returns an empty `data.features` array if no one is sharing or no location data is available yet. ### Path Parameters - `chatId: string` ### Returns - `GetChatLocationResponse object { data, success }` - `data: object { features, type }` - `features: array of object { geometry, properties, type }` - `geometry: object { coordinates, type }` - `coordinates: array of number` [longitude, latitude] or [longitude, latitude, altitude] - `type: "Point"` - `"Point"` - `properties: object { handle, address, locality, updated_at }` - `handle: string` Phone number or email of the person sharing their location - `address: optional string` Full street address - `locality: optional string` City or locality name - `updated_at: optional string` When the location was last updated - `type: "Feature"` - `"Feature"` - `type: "FeatureCollection"` - `"FeatureCollection"` - `success: boolean` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/location \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` --- # Messages URL: https://docs.linqapp.com/api/resources/chats/subresources/messages/ ## Send a message to an existing chat **post** `/v3/chats/{chatId}/messages` Send a message to an existing chat. Use this endpoint when you already have a chat ID and want to send additional messages to it. ## Message Effects You can add iMessage effects to make your messages more expressive. Effects are optional and can be either screen effects (full-screen animations) or bubble effects (message bubble animations). **Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`, `hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight` **Bubble Effects:** `slam`, `loud`, `gentle`, `invisible` Only one effect type can be applied per message. ## Inline Text Decorations (iMessage only) Use the `text_decorations` array on a text part to apply styling and animations to character ranges. Each decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`. **Styles:** `bold`, `italic`, `strikethrough`, `underline` **Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter` ```json { "type": "text", "value": "Hello world", "text_decorations": [ { "range": [0, 5], "style": "bold" }, { "range": [6, 11], "animation": "shake" } ] } ``` **Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. ### Path Parameters - `chatId: string` ### Body Parameters - `message: MessageContent` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. ### Returns - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "message": { "parts": [ { "type": "text", "value": "Hello, world!" } ] } }' ``` #### Response ```json { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" } } ``` ## Get messages from a chat **get** `/v3/chats/{chatId}/messages` Retrieve messages from a specific chat with pagination support. ### Path Parameters - `chatId: string` ### Query Parameters - `cursor: optional string` Pagination cursor from previous next_cursor response - `limit: optional number` Maximum number of messages to return ### Returns - `messages: array of Message` List of messages - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type - `next_cursor: optional string` Cursor for fetching the next page of results. Null if there are no more results to fetch. Pass this value as the `cursor` parameter in the next request. ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "messages": [ { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ], "next_cursor": "next_cursor" } ``` ## Domain Types ### Sent Message - `SentMessage object { id, created_at, delivery_status, 9 more }` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type ### Message Send Response - `MessageSendResponse object { chat_id, message }` Response for sending a message to a chat - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type --- # api/resources/chats/subresources/messages/methods/list/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/messages/methods/list/ ## Get messages from a chat **get** `/v3/chats/{chatId}/messages` Retrieve messages from a specific chat with pagination support. ### Path Parameters - `chatId: string` ### Query Parameters - `cursor: optional string` Pagination cursor from previous next_cursor response - `limit: optional number` Maximum number of messages to return ### Returns - `messages: array of Message` List of messages - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type - `next_cursor: optional string` Cursor for fetching the next page of results. Null if there are no more results to fetch. Pass this value as the `cursor` parameter in the next request. ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "messages": [ { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ], "next_cursor": "next_cursor" } ``` --- # api/resources/chats/subresources/messages/methods/send/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/messages/methods/send/ ## Send a message to an existing chat **post** `/v3/chats/{chatId}/messages` Send a message to an existing chat. Use this endpoint when you already have a chat ID and want to send additional messages to it. ## Message Effects You can add iMessage effects to make your messages more expressive. Effects are optional and can be either screen effects (full-screen animations) or bubble effects (message bubble animations). **Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`, `hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight` **Bubble Effects:** `slam`, `loud`, `gentle`, `invisible` Only one effect type can be applied per message. ## Inline Text Decorations (iMessage only) Use the `text_decorations` array on a text part to apply styling and animations to character ranges. Each decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`. **Styles:** `bold`, `italic`, `strikethrough`, `underline` **Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter` ```json { "type": "text", "value": "Hello world", "text_decorations": [ { "range": [0, 5], "style": "bold" }, { "range": [6, 11], "animation": "shake" } ] } ``` **Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. ### Path Parameters - `chatId: string` ### Body Parameters - `message: MessageContent` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. ### Returns - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "message": { "parts": [ { "type": "text", "value": "Hello, world!" } ] } }' ``` #### Response ```json { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" } } ``` --- # Participants URL: https://docs.linqapp.com/api/resources/chats/subresources/participants/ ## Add a participant to a chat **post** `/v3/chats/{chatId}/participants` Add a new participant to an existing group chat. **Requirements:** - Group chats only (3+ existing participants) - New participant must support the same messaging service as the group - Cross-service additions not allowed (e.g., can't add RCS-only user to iMessage group) - For cross-service scenarios, create a new chat instead ### Path Parameters - `chatId: string` ### Body Parameters - `handle: string` Phone number (E.164 format) or email address of the participant to add ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/participants \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "handle": "+12052499136" }' ``` #### Response ```json { "message": "Participant addition queued", "status": "accepted", "trace_id": "trace_id" } ``` ## Remove a participant from a chat **delete** `/v3/chats/{chatId}/participants` Remove a participant from an existing group chat. **Requirements:** - Group chats only - Must have 3+ participants after removal ### Path Parameters - `chatId: string` ### Body Parameters - `handle: string` Phone number (E.164 format) or email address of the participant to remove ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/participants \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "message": "Participant removal queued", "status": "accepted", "trace_id": "trace_id" } ``` ## Domain Types ### Participant Add Response - `ParticipantAddResponse object { message, status, trace_id }` - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Participant Remove Response - `ParticipantRemoveResponse object { message, status, trace_id }` - `message: optional string` - `status: optional string` - `trace_id: optional string` --- # api/resources/chats/subresources/participants/methods/add/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/participants/methods/add/ ## Add a participant to a chat **post** `/v3/chats/{chatId}/participants` Add a new participant to an existing group chat. **Requirements:** - Group chats only (3+ existing participants) - New participant must support the same messaging service as the group - Cross-service additions not allowed (e.g., can't add RCS-only user to iMessage group) - For cross-service scenarios, create a new chat instead ### Path Parameters - `chatId: string` ### Body Parameters - `handle: string` Phone number (E.164 format) or email address of the participant to add ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/participants \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "handle": "+12052499136" }' ``` #### Response ```json { "message": "Participant addition queued", "status": "accepted", "trace_id": "trace_id" } ``` --- # api/resources/chats/subresources/participants/methods/remove/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/participants/methods/remove/ ## Remove a participant from a chat **delete** `/v3/chats/{chatId}/participants` Remove a participant from an existing group chat. **Requirements:** - Group chats only - Must have 3+ participants after removal ### Path Parameters - `chatId: string` ### Body Parameters - `handle: string` Phone number (E.164 format) or email address of the participant to remove ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/participants \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "message": "Participant removal queued", "status": "accepted", "trace_id": "trace_id" } ``` --- # Typing URL: https://docs.linqapp.com/api/resources/chats/subresources/typing/ ## Start typing indicator **post** `/v3/chats/{chatId}/typing` Send a typing indicator to show that someone is typing in the chat. ## Behavior & Limitations Typing indicators are best-effort signals with the following limitations: - **Active conversations only:** The recipient must have sent or received a message in this chat within the **last 5 minutes**. If the chat is inactive, the request is still accepted (`204`) but the indicator will not reach the recipient's device. - **No delivery guarantee:** Even for active chats, a `204` response only indicates the request was accepted for processing. - **Group chats not supported:** Attempting to start a typing indicator in a group chat will return a `403` error. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/typing \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` ## Stop typing indicator **delete** `/v3/chats/{chatId}/typing` Stop the typing indicator for the chat. Typing indicators are automatically stopped when a message is sent, so calling this endpoint after sending a message is unnecessary. See the `POST` endpoint above for behavior details and limitations. **Note:** Group chats are not supported and will return a `403` error. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/typing \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` --- # api/resources/chats/subresources/typing/methods/start/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/typing/methods/start/ ## Start typing indicator **post** `/v3/chats/{chatId}/typing` Send a typing indicator to show that someone is typing in the chat. ## Behavior & Limitations Typing indicators are best-effort signals with the following limitations: - **Active conversations only:** The recipient must have sent or received a message in this chat within the **last 5 minutes**. If the chat is inactive, the request is still accepted (`204`) but the indicator will not reach the recipient's device. - **No delivery guarantee:** Even for active chats, a `204` response only indicates the request was accepted for processing. - **Group chats not supported:** Attempting to start a typing indicator in a group chat will return a `403` error. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/typing \ -X POST \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` --- # api/resources/chats/subresources/typing/methods/stop/index.md URL: https://docs.linqapp.com/api/resources/chats/subresources/typing/methods/stop/ ## Stop typing indicator **delete** `/v3/chats/{chatId}/typing` Stop the typing indicator for the chat. Typing indicators are automatically stopped when a message is sent, so calling this endpoint after sending a message is unnecessary. See the `POST` endpoint above for behavior details and limitations. **Note:** Group chats are not supported and will return a `403` error. ### Path Parameters - `chatId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/typing \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` --- # Contact Card URL: https://docs.linqapp.com/api/resources/contact_card/ ## Get contact cards **get** `/v3/contact_card` Returns the contact card for a specific phone number, or all contact cards for the authenticated partner if no `phone_number` is provided. ### Query Parameters - `phone_number: optional string` E.164 phone number to filter by. If omitted, all my cards for the partner are returned. ### Returns - `contact_cards: array of object { first_name, is_active, phone_number, 2 more }` - `first_name: string` - `is_active: boolean` - `phone_number: string` - `image_url: optional string` - `last_name: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/contact_card \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "contact_cards": [ { "phone_number": "+15551234567", "first_name": "John", "last_name": "Doe", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "is_active": true } ] } ``` ## Setup contact card **post** `/v3/contact_card` Creates a contact card for a phone number. This endpoint is intended for initial, one-time setup only. The contact card is stored in an inactive state first. Once it's applied successfully, it is activated and `is_active` is returned as `true`. On failure, `is_active` is `false`. **Note:** To update an existing contact card after setup, use `PATCH /v3/contact_card` instead. ### Body Parameters - `first_name: string` First name for the contact card. Required. - `phone_number: string` E.164 phone number to associate the contact card with - `image_url: optional string` Profile image URL for the contact card. - `last_name: optional string` Last name for the contact card. Optional. ### Returns - `SetContactCard object { first_name, is_active, phone_number, 2 more }` - `first_name: string` First name on the contact card - `is_active: boolean` Whether the contact card was successfully applied to the device - `phone_number: string` The phone number the contact card is associated with - `image_url: optional string` Image URL on the contact card - `last_name: optional string` Last name on the contact card ### Example ```http curl https://api.linqapp.com/api/partner/v3/contact_card \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "first_name": "Acme", "phone_number": "+15551234567", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "last_name": "Support" }' ``` #### Response ```json { "phone_number": "+15551234567", "first_name": "John", "last_name": "Doe", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "is_active": true } ``` ## Update contact card **patch** `/v3/contact_card` Partially updates an existing active contact card for a phone number. Fetches the current active contact card and merges the provided fields. Only fields present in the request body are updated; omitted fields retain their existing values. Requires an active contact card to exist for the phone number. ### Query Parameters - `phone_number: string` E.164 phone number of the contact card to update ### Body Parameters - `first_name: optional string` Updated first name. If omitted, the existing value is kept. - `image_url: optional string` Updated profile image URL. If omitted, the existing image is kept. - `last_name: optional string` Updated last name. If omitted, the existing value is kept. ### Returns - `SetContactCard object { first_name, is_active, phone_number, 2 more }` - `first_name: string` First name on the contact card - `is_active: boolean` Whether the contact card was successfully applied to the device - `phone_number: string` The phone number the contact card is associated with - `image_url: optional string` Image URL on the contact card - `last_name: optional string` Last name on the contact card ### Example ```http curl https://api.linqapp.com/api/partner/v3/contact_card \ -X PATCH \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "first_name": "John", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "last_name": "Doe" }' ``` #### Response ```json { "phone_number": "+15551234567", "first_name": "John", "last_name": "Doe", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "is_active": true } ``` ## Domain Types ### Set Contact Card - `SetContactCard object { first_name, is_active, phone_number, 2 more }` - `first_name: string` First name on the contact card - `is_active: boolean` Whether the contact card was successfully applied to the device - `phone_number: string` The phone number the contact card is associated with - `image_url: optional string` Image URL on the contact card - `last_name: optional string` Last name on the contact card ### Contact Card Retrieve Response - `ContactCardRetrieveResponse object { contact_cards }` - `contact_cards: array of object { first_name, is_active, phone_number, 2 more }` - `first_name: string` - `is_active: boolean` - `phone_number: string` - `image_url: optional string` - `last_name: optional string` --- # api/resources/contact_card/methods/create/index.md URL: https://docs.linqapp.com/api/resources/contact_card/methods/create/ ## Setup contact card **post** `/v3/contact_card` Creates a contact card for a phone number. This endpoint is intended for initial, one-time setup only. The contact card is stored in an inactive state first. Once it's applied successfully, it is activated and `is_active` is returned as `true`. On failure, `is_active` is `false`. **Note:** To update an existing contact card after setup, use `PATCH /v3/contact_card` instead. ### Body Parameters - `first_name: string` First name for the contact card. Required. - `phone_number: string` E.164 phone number to associate the contact card with - `image_url: optional string` Profile image URL for the contact card. - `last_name: optional string` Last name for the contact card. Optional. ### Returns - `SetContactCard object { first_name, is_active, phone_number, 2 more }` - `first_name: string` First name on the contact card - `is_active: boolean` Whether the contact card was successfully applied to the device - `phone_number: string` The phone number the contact card is associated with - `image_url: optional string` Image URL on the contact card - `last_name: optional string` Last name on the contact card ### Example ```http curl https://api.linqapp.com/api/partner/v3/contact_card \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "first_name": "Acme", "phone_number": "+15551234567", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "last_name": "Support" }' ``` #### Response ```json { "phone_number": "+15551234567", "first_name": "John", "last_name": "Doe", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "is_active": true } ``` --- # api/resources/contact_card/methods/retrieve/index.md URL: https://docs.linqapp.com/api/resources/contact_card/methods/retrieve/ ## Get contact cards **get** `/v3/contact_card` Returns the contact card for a specific phone number, or all contact cards for the authenticated partner if no `phone_number` is provided. ### Query Parameters - `phone_number: optional string` E.164 phone number to filter by. If omitted, all my cards for the partner are returned. ### Returns - `contact_cards: array of object { first_name, is_active, phone_number, 2 more }` - `first_name: string` - `is_active: boolean` - `phone_number: string` - `image_url: optional string` - `last_name: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/contact_card \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "contact_cards": [ { "phone_number": "+15551234567", "first_name": "John", "last_name": "Doe", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "is_active": true } ] } ``` --- # api/resources/contact_card/methods/update/index.md URL: https://docs.linqapp.com/api/resources/contact_card/methods/update/ ## Update contact card **patch** `/v3/contact_card` Partially updates an existing active contact card for a phone number. Fetches the current active contact card and merges the provided fields. Only fields present in the request body are updated; omitted fields retain their existing values. Requires an active contact card to exist for the phone number. ### Query Parameters - `phone_number: string` E.164 phone number of the contact card to update ### Body Parameters - `first_name: optional string` Updated first name. If omitted, the existing value is kept. - `image_url: optional string` Updated profile image URL. If omitted, the existing image is kept. - `last_name: optional string` Updated last name. If omitted, the existing value is kept. ### Returns - `SetContactCard object { first_name, is_active, phone_number, 2 more }` - `first_name: string` First name on the contact card - `is_active: boolean` Whether the contact card was successfully applied to the device - `phone_number: string` The phone number the contact card is associated with - `image_url: optional string` Image URL on the contact card - `last_name: optional string` Last name on the contact card ### Example ```http curl https://api.linqapp.com/api/partner/v3/contact_card \ -X PATCH \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "first_name": "John", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "last_name": "Doe" }' ``` #### Response ```json { "phone_number": "+15551234567", "first_name": "John", "last_name": "Doe", "image_url": "https://cdn.linqapp.com/contact-card/example.jpg", "is_active": true } ``` --- # Messages URL: https://docs.linqapp.com/api/resources/messages/ ## Send a message (auto-selected from-number) **post** `/v3/messages` Send a message to one or more recipients **without supplying a `from` number**. Linq resolves both the sending line and the target chat for you, then returns exactly which line was used, which chat the message landed in, whether a new chat was created, and every resulting message id. This fuses "create chat" and "send message" behind a single message-centric resource. Provide only the recipients (`to`) and the `message`; the platform decides the rest. ## How the from-number and chat are chosen - **Reuse** — if a chat with exactly these recipients already exists and the line it lives on is healthy, the message is sent into that chat on its existing line (`from_selection.reason = reused_active_chat`). - **New** — if no such chat exists, a new chat is created on the best available line (`from_selection.reason = new_best_number`). - **Failover** — if a matching chat exists but its line has been flagged, a **new** chat is created on a fresh best line and the flagged chat is abandoned (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you supply `continuation_message`, that text is sent as the single message INSTEAD of `message` (useful as a fresh-number-appropriate opener). Exactly one message is sent either way. Recipients (`to`) are an order-independent set: a single handle is a direct chat, multiple handles a group chat. ## Differences from POST /v3/chats - The first message **may contain a link** (including for a newly created chat). Note: sending a link as the very first message on a freshly selected line can elevate that line's flagging risk — it is allowed, not recommended. - Voice memos are **not** supported here. To send an iMessage voice-memo bubble, use `POST /v3/chats/{chatId}/voicememo` with a known chat id. ## Service preference, effects, decorations Set `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`, and per-part `text_decorations` exactly as on the other send endpoints. Always responds `202 Accepted` — chat creation is incidental to the send. ### Header Parameters - `"Idempotency-Key": optional string` ### Body Parameters - `message: MessageContent` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. - `to: array of string` Recipient handles (E.164 phone numbers or email addresses). One handle is a direct chat; multiple handles a group chat. Order-independent — the set identifies the chat. - `continuation_message: optional object { text }` Text-only fallback that **replaces** `message` ONLY on the failover branch — when a chat with these recipients already existed but its line was flagged, so a new chat is created on a fresh line. On that branch this text is sent as the single message instead of `message` (the recipient is on a new number, so you typically want a fresh-number-appropriate opener rather than the original content). Ignored otherwise (a healthy reuse, or genuine first contact). Carries no parts, media, or effects — exactly one message is ever sent. - `text: string` The replacement message text, sent as the single message on failover. ### Returns - `chat_id: string` The resolved chat (reused or newly created) the message landed in. - `created_new_chat: boolean` True when a new chat was created (new or failover), false on reuse. - `from: string` The line (E.164) the message was actually sent from. - `from_selection: object { reason, reused_existing_chat }` Why this line/chat was chosen. - `reason: "reused_active_chat" or "new_best_number" or "failover_flagged"` - `reused_active_chat` — reused an existing chat on its healthy line - `new_best_number` — created a new chat on the best available line - `failover_flagged` — prior chat's line was flagged; created a new chat on a fresh line - `"reused_active_chat"` - `"new_best_number"` - `"failover_flagged"` - `reused_existing_chat: boolean` True only when an existing chat was reused. - `handles: array of ChatHandle` Participants of the resolved chat. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_group: boolean` Whether the resolved chat is a group chat. - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `is_me: optional boolean` Whether this handle belongs to the sender (your phone number) - `left_at: optional string` When they left (if applicable) - `status: optional "active" or "left" or "removed"` Participant status - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type - `service: ServiceType` Messaging service type - `previous_chat_id: optional string` Set ONLY on `failover_flagged`: the abandoned flagged chat that was NOT sent into. Null otherwise. ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "message": { "parts": [ { "type": "text", "value": "Hi! Thanks for reaching out — how can we help?" } ] }, "to": [ "+14155559876" ] }' ``` #### Response ```json { "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_new_chat": false, "from": "+12052535597", "from_selection": { "reason": "reused_active_chat", "reused_existing_chat": true }, "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" }, "service": "iMessage", "previous_chat_id": null } ``` ## Get all messages in a thread **get** `/v3/messages/{messageId}/thread` Retrieve all messages in a conversation thread. Given any message ID in the thread, returns the originator message and all replies in chronological order. If the message is not part of a thread, returns just that single message. Supports pagination and configurable ordering. ### Path Parameters - `messageId: string` ### Query Parameters - `cursor: optional string` Pagination cursor from previous next_cursor response - `limit: optional number` Maximum number of messages to return - `order: optional "asc" or "desc"` Sort order for messages (asc = oldest first, desc = newest first) - `"asc"` - `"desc"` ### Returns - `messages: array of Message` Messages in the thread, ordered by the specified order parameter - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type - `next_cursor: optional string` Cursor for fetching the next page of results (null if no more results) ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/thread \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "messages": [ { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ], "next_cursor": "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0cyI6MTYzMDUwMDAwMH0=" } ``` ## Get a message by ID **get** `/v3/messages/{messageId}` Retrieve a specific message by its ID. This endpoint returns the full message details including text, attachments, reactions, and metadata. ### Path Parameters - `messageId: string` ### Returns - `Message object { id, chat_id, created_at, 15 more }` - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ``` ## Delete a message from system **delete** `/v3/messages/{messageId}` Deletes a message from the Linq API only. This does NOT unsend or remove the message from the actual chat — recipients will still see the message. ### Path Parameters - `messageId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` ## Add or remove a reaction to a message **post** `/v3/messages/{messageId}/reactions` Add or remove emoji reactions to messages. Reactions let users express their response to a message without sending a new message. **Supported Reactions:** - love ❤️ - like 👍 - dislike 👎 - laugh 😂 - emphasize ‼️ - question ❓ - custom - any emoji (use `custom_emoji` field to specify) ### Path Parameters - `messageId: string` ### Body Parameters - `operation: "add" or "remove"` Whether to add or remove the reaction - `"add"` - `"remove"` - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji string. Required when type is "custom". - `part_index: optional number` Optional index of the message part to react to. If not provided, reacts to the entire message (part 0). ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/reactions \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "operation": "add", "type": "love", "part_index": 1 }' ``` #### Response ```json { "message": "Reaction processed", "status": "accepted", "trace_id": "trace_id" } ``` ## Edit the content of a message part **patch** `/v3/messages/{messageId}` Edit the text content of a specific part of a previously sent message. **Note:** A message can be edited up to 5 times, and only within 15 minutes of when it was originally sent. ### Path Parameters - `messageId: string` ### Body Parameters - `text: string` New text content for the message part - `part_index: optional number` Index of the message part to edit. Defaults to 0. ### Returns - `Message object { id, chat_id, created_at, 15 more }` - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID \ -X PATCH \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "text": "This is the edited message content" }' ``` #### Response ```json { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ``` ## Update an iMessage app card in place **post** `/v3/messages/{messageId}/update` Replaces a previously delivered `imessage_app` card on the recipient's screen with new content, instead of posting a new bubble (like a game move redrawing the board). The update is delivered as a **new message** with its own id and delivery lifecycle (`message.sent` / `message.delivered` / `message.failed` webhooks fire for the new id). To update the card again, reference the message id returned by this call. Constraints: - The referenced message must be an `imessage_app` card sent by you (`400` otherwise — inbound cards cannot be updated). - The referenced card must already be delivered (`409` otherwise — retry after the `message.delivered` webhook for it). - The app identity (`team_id`, `bundle_id`, name) is inherited from the original card and cannot change; only `url`, `fallback_text`, and `layout` are replaced. - iMessage-only, like all app cards. - Concurrent updates against the same card are not serialized server-side; the last one delivered wins on the recipient's screen. Serialize updates by always referencing the message id returned by the previous call. ### Path Parameters - `messageId: string` ### Body Parameters - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the updated card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live view; `false` always shows the static `layout` card. Recipients without your app always see the static card regardless of this flag. Defaults to `true` when omitted — it is **not** inherited from the original card. To keep a card static across updates, re-send `interactive: false` on each update. - `url: optional string` URL the recipient's app opens when they tap the updated card. ### Returns - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/update \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "layout": { "caption": "Score: 2 – 1" }, "fallback_text": "Score update", "interactive": true, "url": "https://app.example.com/card?game=7f3a&move=2" }' ``` #### Response ```json { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" } } ``` ## Domain Types ### Message - `Message object { id, chat_id, created_at, 15 more }` - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type ### Message Effect - `MessageEffect object { name, type }` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` ### Reply To - `ReplyTo object { 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 - `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. ### Message Create Response - `MessageCreateResponse object { chat_id, created_new_chat, from, 6 more }` Result of an auto-from send. Self-describing: which line was used, which chat the message landed in, whether a new chat was created, and the resulting message id(s). - `chat_id: string` The resolved chat (reused or newly created) the message landed in. - `created_new_chat: boolean` True when a new chat was created (new or failover), false on reuse. - `from: string` The line (E.164) the message was actually sent from. - `from_selection: object { reason, reused_existing_chat }` Why this line/chat was chosen. - `reason: "reused_active_chat" or "new_best_number" or "failover_flagged"` - `reused_active_chat` — reused an existing chat on its healthy line - `new_best_number` — created a new chat on the best available line - `failover_flagged` — prior chat's line was flagged; created a new chat on a fresh line - `"reused_active_chat"` - `"new_best_number"` - `"failover_flagged"` - `reused_existing_chat: boolean` True only when an existing chat was reused. - `handles: array of ChatHandle` Participants of the resolved chat. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_group: boolean` Whether the resolved chat is a group chat. - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `is_me: optional boolean` Whether this handle belongs to the sender (your phone number) - `left_at: optional string` When they left (if applicable) - `status: optional "active" or "left" or "removed"` Participant status - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type - `service: ServiceType` Messaging service type - `previous_chat_id: optional string` Set ONLY on `failover_flagged`: the abandoned flagged chat that was NOT sent into. Null otherwise. ### Message Add Reaction Response - `MessageAddReactionResponse object { message, status, trace_id }` - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Message Update App Card Response - `MessageUpdateAppCardResponse object { chat_id, message }` Response for sending a message to a chat - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type --- # api/resources/messages/methods/add_reaction/index.md URL: https://docs.linqapp.com/api/resources/messages/methods/add_reaction/ ## Add or remove a reaction to a message **post** `/v3/messages/{messageId}/reactions` Add or remove emoji reactions to messages. Reactions let users express their response to a message without sending a new message. **Supported Reactions:** - love ❤️ - like 👍 - dislike 👎 - laugh 😂 - emphasize ‼️ - question ❓ - custom - any emoji (use `custom_emoji` field to specify) ### Path Parameters - `messageId: string` ### Body Parameters - `operation: "add" or "remove"` Whether to add or remove the reaction - `"add"` - `"remove"` - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji string. Required when type is "custom". - `part_index: optional number` Optional index of the message part to react to. If not provided, reacts to the entire message (part 0). ### Returns - `message: optional string` - `status: optional string` - `trace_id: optional string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/reactions \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "operation": "add", "type": "love", "part_index": 1 }' ``` #### Response ```json { "message": "Reaction processed", "status": "accepted", "trace_id": "trace_id" } ``` --- # api/resources/messages/methods/create/index.md URL: https://docs.linqapp.com/api/resources/messages/methods/create/ ## Send a message (auto-selected from-number) **post** `/v3/messages` Send a message to one or more recipients **without supplying a `from` number**. Linq resolves both the sending line and the target chat for you, then returns exactly which line was used, which chat the message landed in, whether a new chat was created, and every resulting message id. This fuses "create chat" and "send message" behind a single message-centric resource. Provide only the recipients (`to`) and the `message`; the platform decides the rest. ## How the from-number and chat are chosen - **Reuse** — if a chat with exactly these recipients already exists and the line it lives on is healthy, the message is sent into that chat on its existing line (`from_selection.reason = reused_active_chat`). - **New** — if no such chat exists, a new chat is created on the best available line (`from_selection.reason = new_best_number`). - **Failover** — if a matching chat exists but its line has been flagged, a **new** chat is created on a fresh best line and the flagged chat is abandoned (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you supply `continuation_message`, that text is sent as the single message INSTEAD of `message` (useful as a fresh-number-appropriate opener). Exactly one message is sent either way. Recipients (`to`) are an order-independent set: a single handle is a direct chat, multiple handles a group chat. ## Differences from POST /v3/chats - The first message **may contain a link** (including for a newly created chat). Note: sending a link as the very first message on a freshly selected line can elevate that line's flagging risk — it is allowed, not recommended. - Voice memos are **not** supported here. To send an iMessage voice-memo bubble, use `POST /v3/chats/{chatId}/voicememo` with a known chat id. ## Service preference, effects, decorations Set `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`, and per-part `text_decorations` exactly as on the other send endpoints. Always responds `202 Accepted` — chat creation is incidental to the send. ### Header Parameters - `"Idempotency-Key": optional string` ### Body Parameters - `message: MessageContent` 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 or MediaPart or LinkPart or object { app, layout, type, 3 more }` Array of message parts. Each part can be text, media, or link. Parts are displayed in order. Text and media can be mixed freely, but a `link` part must be the only part in the message. **Rich Link Previews:** - Use a `link` part to send a URL with a rich preview card - A `link` part must be the **only** part in the message - To send a URL as plain text (no preview), use a `text` part instead **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 - To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated `/v3/chats/{chatId}/voicememo` endpoint instead **Validation Rules:** - A `link` part must be the **only** part in the message. It cannot be combined with text or media parts. - Consecutive text parts are not allowed. Text parts must be separated by media parts. For example, [text, text] is invalid, but [text, media, text] is valid. - Maximum of **100 parts** total. - Media parts using a public `url` (downloaded by the server on send) are capped at **40**. Parts using `attachment_id` or presigned URLs are exempt from this sub-limit. For bulk media sends exceeding 40 files, pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`. - `TextPart object { type, value, text_decorations }` - `type: "text"` Indicates this is a text message part - `"text"` - `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. Use `text_decorations` to apply inline formatting and animations (iMessage only). - `text_decorations: optional array of TextDecoration` Optional array of text decorations applied to character ranges in the `value` field (iMessage only). Each decoration specifies a character range `[start, end)` and exactly one of `style` or `animation`. **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 text), but animation ranges must not overlap with other animations or styles. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* **Note:** Text decorations only render for iMessage recipients. For SMS/RCS, text decorations are not applied. - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPart object { type, attachment_id, url }` - `type: "media"` Indicates this is a media attachment part - `"media"` - `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. - `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. To send audio as an iMessage voice memo bubble (with inline playback), use `/v3/chats/{chatId}/voicememo`. 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. - `LinkPart object { type, value }` - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` URL to send with a rich link preview. The recipient will see an inline card with the page's title, description, and preview image (when available). A `link` part must be the **only** part in the message. To send a URL as plain text (no preview card), use a `text` part instead. - `IMessageApp object { app, layout, type, 3 more }` An iMessage app card, backed by a Messages app extension. iMessage only — an `imessage_app` part must be the **only** part in the message and is never delivered over SMS/RCS. See the IMessageAppServiceUnsupported (2018) and RecipientUnsupportedMessageType (4005) error codes. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live, interactive view for those recipients; everyone else sees the static card built from `layout`. `false` always shows the static `layout` card, even to recipients who have the app installed. Recipients without your app always see the static card regardless of this flag. - `url: optional string` URL the recipient's app opens when they tap the card. - `effect: optional MessageEffect` iMessage effect to apply to this message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `idempotency_key: optional string` Optional idempotency key for this message. Use this to prevent duplicate sends of the same message. - `preferred_service: optional ServiceType` Messaging service type - `"iMessage"` - `"SMS"` - `"RCS"` - `reply_to: optional ReplyTo` Reply to another message to create a threaded conversation - `message_id: string` The ID of the message to reply to - `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. - `to: array of string` Recipient handles (E.164 phone numbers or email addresses). One handle is a direct chat; multiple handles a group chat. Order-independent — the set identifies the chat. - `continuation_message: optional object { text }` Text-only fallback that **replaces** `message` ONLY on the failover branch — when a chat with these recipients already existed but its line was flagged, so a new chat is created on a fresh line. On that branch this text is sent as the single message instead of `message` (the recipient is on a new number, so you typically want a fresh-number-appropriate opener rather than the original content). Ignored otherwise (a healthy reuse, or genuine first contact). Carries no parts, media, or effects — exactly one message is ever sent. - `text: string` The replacement message text, sent as the single message on failover. ### Returns - `chat_id: string` The resolved chat (reused or newly created) the message landed in. - `created_new_chat: boolean` True when a new chat was created (new or failover), false on reuse. - `from: string` The line (E.164) the message was actually sent from. - `from_selection: object { reason, reused_existing_chat }` Why this line/chat was chosen. - `reason: "reused_active_chat" or "new_best_number" or "failover_flagged"` - `reused_active_chat` — reused an existing chat on its healthy line - `new_best_number` — created a new chat on the best available line - `failover_flagged` — prior chat's line was flagged; created a new chat on a fresh line - `"reused_active_chat"` - `"new_best_number"` - `"failover_flagged"` - `reused_existing_chat: boolean` True only when an existing chat was reused. - `handles: array of ChatHandle` Participants of the resolved chat. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_group: boolean` Whether the resolved chat is a group chat. - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `is_me: optional boolean` Whether this handle belongs to the sender (your phone number) - `left_at: optional string` When they left (if applicable) - `status: optional "active" or "left" or "removed"` Participant status - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type - `service: ServiceType` Messaging service type - `previous_chat_id: optional string` Set ONLY on `failover_flagged`: the abandoned flagged chat that was NOT sent into. Null otherwise. ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "message": { "parts": [ { "type": "text", "value": "Hi! Thanks for reaching out — how can we help?" } ] }, "to": [ "+14155559876" ] }' ``` #### Response ```json { "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_new_chat": false, "from": "+12052535597", "from_selection": { "reason": "reused_active_chat", "reused_existing_chat": true }, "handles": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" } ], "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" }, "service": "iMessage", "previous_chat_id": null } ``` --- # api/resources/messages/methods/delete/index.md URL: https://docs.linqapp.com/api/resources/messages/methods/delete/ ## Delete a message from system **delete** `/v3/messages/{messageId}` Deletes a message from the Linq API only. This does NOT unsend or remove the message from the actual chat — recipients will still see the message. ### Path Parameters - `messageId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 400, "code": 1002, "message": "Phone number must be in E.164 format", "doc_url": "https://docs.linqapp.com/error/codes/1xxx/1002/" }, "success": false } ``` --- # api/resources/messages/methods/list_messages_thread/index.md URL: https://docs.linqapp.com/api/resources/messages/methods/list_messages_thread/ ## Get all messages in a thread **get** `/v3/messages/{messageId}/thread` Retrieve all messages in a conversation thread. Given any message ID in the thread, returns the originator message and all replies in chronological order. If the message is not part of a thread, returns just that single message. Supports pagination and configurable ordering. ### Path Parameters - `messageId: string` ### Query Parameters - `cursor: optional string` Pagination cursor from previous next_cursor response - `limit: optional number` Maximum number of messages to return - `order: optional "asc" or "desc"` Sort order for messages (asc = oldest first, desc = newest first) - `"asc"` - `"desc"` ### Returns - `messages: array of Message` Messages in the thread, ordered by the specified order parameter - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type - `next_cursor: optional string` Cursor for fetching the next page of results (null if no more results) ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/thread \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "messages": [ { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ], "next_cursor": "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0cyI6MTYzMDUwMDAwMH0=" } ``` --- # api/resources/messages/methods/retrieve/index.md URL: https://docs.linqapp.com/api/resources/messages/methods/retrieve/ ## Get a message by ID **get** `/v3/messages/{messageId}` Retrieve a specific message by its ID. This endpoint returns the full message details including text, attachments, reactions, and metadata. ### Path Parameters - `messageId: string` ### Returns - `Message object { id, chat_id, created_at, 15 more }` - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ``` --- # api/resources/messages/methods/update_app_card/index.md URL: https://docs.linqapp.com/api/resources/messages/methods/update_app_card/ ## Update an iMessage app card in place **post** `/v3/messages/{messageId}/update` Replaces a previously delivered `imessage_app` card on the recipient's screen with new content, instead of posting a new bubble (like a game move redrawing the board). The update is delivered as a **new message** with its own id and delivery lifecycle (`message.sent` / `message.delivered` / `message.failed` webhooks fire for the new id). To update the card again, reference the message id returned by this call. Constraints: - The referenced message must be an `imessage_app` card sent by you (`400` otherwise — inbound cards cannot be updated). - The referenced card must already be delivered (`409` otherwise — retry after the `message.delivered` webhook for it). - The app identity (`team_id`, `bundle_id`, name) is inherited from the original card and cannot change; only `url`, `fallback_text`, and `layout` are replaced. - iMessage-only, like all app cards. - Concurrent updates against the same card are not serialized server-side; the last one delivered wins on the recipient's screen. Serialize updates by always referencing the message id returned by the previous call. ### Path Parameters - `messageId: string` ### Body Parameters - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `fallback_text: optional string` Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. - `interactive: optional boolean` Whether the updated card renders as your app's interactive balloon for recipients who have your iMessage app installed. `true` (default) lets your installed extension draw its live view; `false` always shows the static `layout` card. Recipients without your app always see the static card regardless of this flag. Defaults to `true` when omitted — it is **not** inherited from the original card. To keep a card static across updates, re-send `interactive: false` on each update. - `url: optional string` URL the recipient's app opens when they tap the updated card. ### Returns - `chat_id: string` Unique identifier of the chat this message was sent to - `message: SentMessage` A message that was sent (used in CreateChat and SendMessage responses) - `id: string` Message identifier (UUID) - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `parts: array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sent_at: string` When the message was actually sent (null if still queued) - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `preferred_service: optional ServiceType` Messaging service type - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/update \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "layout": { "caption": "Score: 2 – 1" }, "fallback_text": "Score update", "interactive": true, "url": "https://app.example.com/card?game=7f3a&move=2" }' ``` #### Response ```json { "chat_id": "550e8400-e29b-41d4-a716-446655440000", "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": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "sent_at": null, "delivered_at": null, "effect": { "name": "confetti", "type": "screen" }, "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "preferred_service": "iMessage", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "service": "iMessage" } } ``` --- # api/resources/messages/methods/update/index.md URL: https://docs.linqapp.com/api/resources/messages/methods/update/ ## Edit the content of a message part **patch** `/v3/messages/{messageId}` Edit the text content of a specific part of a previously sent message. **Note:** A message can be edited up to 5 times, and only within 15 minutes of when it was originally sent. ### Path Parameters - `messageId: string` ### Body Parameters - `text: string` New text content for the message part - `part_index: optional number` Index of the message part to edit. Defaults to 0. ### Returns - `Message object { id, chat_id, created_at, 15 more }` - `id: string` Unique identifier for the message - `chat_id: string` ID of the chat this message belongs to - `created_at: string` When the message was created - `delivery_status: "pending" or "queued" or "sent" or 4 more` Current delivery status of a message - `"pending"` - `"queued"` - `"sent"` - `"delivered"` - `"received"` - `"read"` - `"failed"` - `is_delivered: boolean` DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is `delivered` or `read`). Whether the message has been delivered. - `is_from_me: boolean` Whether this message was sent by the authenticated user - `is_read: boolean` DEPRECATED: Use `delivery_status == "read"` instead. Whether the message has been read. - `updated_at: string` When the message was last updated - `delivered_at: optional string` When the message was delivered - `effect: optional MessageEffect` iMessage effect applied to a message (screen or bubble effect) - `name: optional string` Name of the effect. Common values: - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, love, balloons, happy_birthday, echo, spotlight - Bubble effects: slam, loud, gentle, invisible - `type: optional "screen" or "bubble"` Type of effect - `"screen"` - `"bubble"` - `from: optional string` DEPRECATED: Use from_handle instead. Phone number of the message sender. - `from_handle: optional ChatHandle` The sender of this message as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `parts: optional array of TextPartResponse or MediaPartResponse or LinkPartResponse or object { app, layout, reactions, 3 more }` Message parts in order (text, media, and link) - `TextPartResponse object { reactions, type, value, text_decorations }` A text message part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `MediaPartResponse object { id, filename, mime_type, 4 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `LinkPartResponse object { reactions, type, value }` A rich link preview part - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageAppPartResponse object { app, layout, reactions, 3 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. Must not contain `:`. - `name: string` Display name of the app, shown by Messages' fallback UI. - `team_id: string` The app's 10-character uppercase alphanumeric team identifier. - `app_store_id: optional number` The owning app's App Store id (optional). When set, recipients without the iMessage app installed see a "Get the app" affordance. - `layout: object { caption, image_subtitle, image_title, 4 more }` Visible layout of the card. At least one of `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as an empty bubble. `image_url` displays a preview image at the top of the card. The image renders on the recipient's card whether or not they have your app installed. The small icon beside the caption is the app's own icon and is not settable here. `* Note - requires a trusted chat w/ inbound activity` `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle beneath it). They only appear when `image_url` is set — without an image there is nothing to overlay — so setting either without `image_url` is rejected. - `caption: optional string` Primary label, top-left and bold. - `image_subtitle: optional string` Text shown below `image_title`, overlaid on the card image. Requires `image_url`. - `image_title: optional string` Bold text overlaid on the card image. Requires `image_url` (rejected without it). - `image_url: optional string` URL of a JPEG image to display as the card's preview image; an unreachable or non-image URL returns a validation error. Renders for all recipients regardless of whether they have the app. Note - requires a trusted chat w/ inbound activity. - `subcaption: optional string` Secondary label, below `caption` on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below `trailing_caption`, on the right. - `reactions: array of Reaction` Reactions on this message part - `handle: ChatHandle` - `is_me: boolean` Whether this reaction is from the current user - `type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `custom_emoji: optional string` Custom emoji if type is "custom", null otherwise - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL delivered to the iMessage app on tap. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `preferred_service: optional ServiceType` Messaging service type - `read_at: optional string` When the message was read - `reply_to: optional ReplyTo` Indicates this message is a threaded reply to another message - `message_id: string` The ID of the message to reply to - `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. - `sent_at: optional string` When the message was sent - `service: optional ServiceType` Messaging service type ### Example ```http curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID \ -X PATCH \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "text": "This is the edited message content" }' ``` #### Response ```json { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "chat_id": "94c6bf33-31d9-40e3-a0e9-f94250ecedb9", "created_at": "2024-01-15T10:30:00Z", "delivery_status": "pending", "is_delivered": true, "is_from_me": true, "is_read": false, "updated_at": "2024-01-15T10:30:00Z", "delivered_at": "2024-01-15T10:30:10Z", "effect": { "name": "confetti", "type": "screen" }, "from": "+12052535597", "from_handle": { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "parts": [ { "reactions": [ { "handle": { "id": "69a37c7d-af4f-4b5e-af42-e28e98ce873a", "handle": "+15551234567", "joined_at": "2025-05-21T15:30:00.000-05:00", "service": "iMessage", "is_me": false, "left_at": "2019-12-27T18:11:19.117Z", "status": "active" }, "is_me": false, "type": "love", "custom_emoji": null, "sticker": { "file_name": "sticker.png", "height": 420, "mime_type": "image/png", "url": "https://cdn.linqapp.com/attachments/a1b2c3d4/sticker.png?signature=...", "width": 420 } } ], "type": "text", "value": "Hello!", "text_decorations": [ { "range": [ 0, 5 ], "animation": "shake", "style": "bold" } ] } ], "preferred_service": "iMessage", "read_at": "2024-01-15T10:35:00Z", "reply_to": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "part_index": 0 }, "sent_at": "2024-01-15T10:30:05Z", "service": "iMessage" } ``` --- # Phone Numbers URL: https://docs.linqapp.com/api/resources/phone_numbers/ ## List phone numbers **get** `/v3/phone_numbers` Returns all phone numbers assigned to the authenticated partner. Use this endpoint to discover which phone numbers are available for use as the `from` field when creating a chat, listing chats, or sending a voice memo. ### Returns - `phone_numbers: array of object { id, health_status, phone_number, 2 more }` List of phone numbers assigned to the partner - `id: string` Unique identifier for the phone number - `health_status: object { doc_url, status }` **[BETA]** Current reputation for a phone line. Always present — lines start at `HEALTHY` and may shift based on aggregate engagement and delivery signals across all conversations on the line. Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. - `doc_url: string` Deep-link to the relevant section of the Phone Reputation guide for this status. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `phone_number: string` Phone number in E.164 format - `reputation: object { doc_url, status }` **[BETA]** Current reputation for a phone line. Always present — lines start at `HEALTHY` and may shift based on aggregate engagement and delivery signals across all conversations on the line. Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. - `doc_url: string` Deep-link to the relevant section of the Phone Reputation guide for this status. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `forwarding_number: optional string` The forwarding number associated with this phone number, in E.164 format. Null when no forwarding number is configured. ### Example ```http curl https://api.linqapp.com/api/partner/v3/phone_numbers \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "phone_numbers": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "phone_number": "+12025551234", "forwarding_number": "+12025559999", "reputation": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#healthy" }, "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#healthy" } }, { "id": "550e8400-e29b-41d4-a716-446655440001", "phone_number": "+12025559876", "forwarding_number": null, "reputation": { "status": "AT_RISK", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#at-risk" }, "health_status": { "status": "AT_RISK", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#at-risk" } } ] } ``` ## Update a phone number **put** `/v3/phone_numbers/{phoneNumberId}` Updates the forwarding number for a phone number. The forwarding number is where inbound calls will be forwarded to. Pass an empty string to clear the forwarding number. ### Path Parameters - `phoneNumberId: string` ### Body Parameters - `forwarding_number: string` The forwarding number in E.164 format. Set to null or empty string to clear. ### Returns - `id: string` Unique identifier for the phone number - `forwarding_number: string` The forwarding number after the update. Null when cleared. - `phone_number: string` Phone number in E.164 format ### Example ```http curl https://api.linqapp.com/api/partner/v3/phone_numbers/$PHONE_NUMBER_ID \ -X PUT \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "forwarding_number": "+12025559999" }' ``` #### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "phone_number": "+12025551234", "forwarding_number": "+12025559999" } ``` ## Domain Types ### Phone Number List Response - `PhoneNumberListResponse object { phone_numbers }` - `phone_numbers: array of object { id, health_status, phone_number, 2 more }` List of phone numbers assigned to the partner - `id: string` Unique identifier for the phone number - `health_status: object { doc_url, status }` **[BETA]** Current reputation for a phone line. Always present — lines start at `HEALTHY` and may shift based on aggregate engagement and delivery signals across all conversations on the line. Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. - `doc_url: string` Deep-link to the relevant section of the Phone Reputation guide for this status. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `phone_number: string` Phone number in E.164 format - `reputation: object { doc_url, status }` **[BETA]** Current reputation for a phone line. Always present — lines start at `HEALTHY` and may shift based on aggregate engagement and delivery signals across all conversations on the line. Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. - `doc_url: string` Deep-link to the relevant section of the Phone Reputation guide for this status. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `forwarding_number: optional string` The forwarding number associated with this phone number, in E.164 format. Null when no forwarding number is configured. ### Phone Number Update Response - `PhoneNumberUpdateResponse object { id, forwarding_number, phone_number }` - `id: string` Unique identifier for the phone number - `forwarding_number: string` The forwarding number after the update. Null when cleared. - `phone_number: string` Phone number in E.164 format --- # api/resources/phone_numbers/methods/list/index.md URL: https://docs.linqapp.com/api/resources/phone_numbers/methods/list/ ## List phone numbers **get** `/v3/phone_numbers` Returns all phone numbers assigned to the authenticated partner. Use this endpoint to discover which phone numbers are available for use as the `from` field when creating a chat, listing chats, or sending a voice memo. ### Returns - `phone_numbers: array of object { id, health_status, phone_number, 2 more }` List of phone numbers assigned to the partner - `id: string` Unique identifier for the phone number - `health_status: object { doc_url, status }` **[BETA]** Current reputation for a phone line. Always present — lines start at `HEALTHY` and may shift based on aggregate engagement and delivery signals across all conversations on the line. Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. - `doc_url: string` Deep-link to the relevant section of the Phone Reputation guide for this status. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `phone_number: string` Phone number in E.164 format - `reputation: object { doc_url, status }` **[BETA]** Current reputation for a phone line. Always present — lines start at `HEALTHY` and may shift based on aggregate engagement and delivery signals across all conversations on the line. Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. - `doc_url: string` Deep-link to the relevant section of the Phone Reputation guide for this status. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `forwarding_number: optional string` The forwarding number associated with this phone number, in E.164 format. Null when no forwarding number is configured. ### Example ```http curl https://api.linqapp.com/api/partner/v3/phone_numbers \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "phone_numbers": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "phone_number": "+12025551234", "forwarding_number": "+12025559999", "reputation": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#healthy" }, "health_status": { "status": "HEALTHY", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#healthy" } }, { "id": "550e8400-e29b-41d4-a716-446655440001", "phone_number": "+12025559876", "forwarding_number": null, "reputation": { "status": "AT_RISK", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#at-risk" }, "health_status": { "status": "AT_RISK", "doc_url": "https://docs.linqapp.com/guides/phone-numbers/phone-reputation#at-risk" } } ] } ``` --- # api/resources/phone_numbers/methods/update/index.md URL: https://docs.linqapp.com/api/resources/phone_numbers/methods/update/ ## Update a phone number **put** `/v3/phone_numbers/{phoneNumberId}` Updates the forwarding number for a phone number. The forwarding number is where inbound calls will be forwarded to. Pass an empty string to clear the forwarding number. ### Path Parameters - `phoneNumberId: string` ### Body Parameters - `forwarding_number: string` The forwarding number in E.164 format. Set to null or empty string to clear. ### Returns - `id: string` Unique identifier for the phone number - `forwarding_number: string` The forwarding number after the update. Null when cleared. - `phone_number: string` Phone number in E.164 format ### Example ```http curl https://api.linqapp.com/api/partner/v3/phone_numbers/$PHONE_NUMBER_ID \ -X PUT \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "forwarding_number": "+12025559999" }' ``` #### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "phone_number": "+12025551234", "forwarding_number": "+12025559999" } ``` --- # Phonenumbers URL: https://docs.linqapp.com/api/resources/phonenumbers/ ## List phone numbers (deprecated) **get** `/v3/phonenumbers` **Deprecated.** Use `GET /v3/phone_numbers` instead. ### Returns - `phone_numbers: array of object { id, phone_number, capabilities, 2 more }` List of phone numbers assigned to the partner - `id: string` Unique identifier for the phone number - `phone_number: string` Phone number in E.164 format - `capabilities: optional object { mms, sms, voice }` - `mms: boolean` Whether MMS messaging is supported - `sms: boolean` Whether SMS messaging is supported - `voice: boolean` Whether voice calls are supported - `country_code: optional string` Deprecated. Always null. - `type: optional string` Deprecated. Always null. ### Example ```http curl https://api.linqapp.com/api/partner/v3/phonenumbers \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "phone_numbers": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "phone_number": "+12025551234", "capabilities": { "mms": true, "sms": true, "voice": false }, "country_code": "US", "type": null } ] } ``` ## Domain Types ### Phonenumber List Response - `PhonenumberListResponse object { phone_numbers }` - `phone_numbers: array of object { id, phone_number, capabilities, 2 more }` List of phone numbers assigned to the partner - `id: string` Unique identifier for the phone number - `phone_number: string` Phone number in E.164 format - `capabilities: optional object { mms, sms, voice }` - `mms: boolean` Whether MMS messaging is supported - `sms: boolean` Whether SMS messaging is supported - `voice: boolean` Whether voice calls are supported - `country_code: optional string` Deprecated. Always null. - `type: optional string` Deprecated. Always null. --- # api/resources/phonenumbers/methods/list/index.md URL: https://docs.linqapp.com/api/resources/phonenumbers/methods/list/ ## List phone numbers (deprecated) **get** `/v3/phonenumbers` **Deprecated.** Use `GET /v3/phone_numbers` instead. ### Returns - `phone_numbers: array of object { id, phone_number, capabilities, 2 more }` List of phone numbers assigned to the partner - `id: string` Unique identifier for the phone number - `phone_number: string` Phone number in E.164 format - `capabilities: optional object { mms, sms, voice }` - `mms: boolean` Whether MMS messaging is supported - `sms: boolean` Whether SMS messaging is supported - `voice: boolean` Whether voice calls are supported - `country_code: optional string` Deprecated. Always null. - `type: optional string` Deprecated. Always null. ### Example ```http curl https://api.linqapp.com/api/partner/v3/phonenumbers \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "phone_numbers": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "phone_number": "+12025551234", "capabilities": { "mms": true, "sms": true, "voice": false }, "country_code": "US", "type": null } ] } ``` --- # Webhook Events URL: https://docs.linqapp.com/api/resources/webhook_events/ ## List available webhook event types **get** `/v3/webhook-events` Returns all available webhook event types that can be subscribed to. Use this endpoint to discover valid values for the `subscribed_events` field when creating or updating webhook subscriptions. ### Returns - `doc_url: "https://docs.linqapp.com/guides/webhooks/events"` URL to the webhook events documentation - `"https://docs.linqapp.com/guides/webhooks/events"` - `events: array of WebhookEventType` List of all available webhook event types - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-events \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "events": [ "message.sent", "message.received", "message.read", "message.delivered", "message.failed", "reaction.added", "reaction.removed", "participant.added", "participant.removed", "chat.created", "chat.group_name_updated", "chat.group_icon_updated", "chat.group_name_update_failed", "chat.group_icon_update_failed", "chat.typing_indicator.started", "chat.typing_indicator.stopped", "message.edited", "phone_number.status_updated" ], "doc_url": "https://docs.linqapp.com/guides/webhooks/events" } ``` ## Domain Types ### Webhook Event Type - `WebhookEventType = "message.sent" or "message.received" or "message.read" or 24 more` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` ### Webhook Event List Response - `WebhookEventListResponse object { doc_url, events }` - `doc_url: "https://docs.linqapp.com/guides/webhooks/events"` URL to the webhook events documentation - `"https://docs.linqapp.com/guides/webhooks/events"` - `events: array of WebhookEventType` List of all available webhook event types - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` --- # api/resources/webhook_events/methods/list/index.md URL: https://docs.linqapp.com/api/resources/webhook_events/methods/list/ ## List available webhook event types **get** `/v3/webhook-events` Returns all available webhook event types that can be subscribed to. Use this endpoint to discover valid values for the `subscribed_events` field when creating or updating webhook subscriptions. ### Returns - `doc_url: "https://docs.linqapp.com/guides/webhooks/events"` URL to the webhook events documentation - `"https://docs.linqapp.com/guides/webhooks/events"` - `events: array of WebhookEventType` List of all available webhook event types - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-events \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "events": [ "message.sent", "message.received", "message.read", "message.delivered", "message.failed", "reaction.added", "reaction.removed", "participant.added", "participant.removed", "chat.created", "chat.group_name_updated", "chat.group_icon_updated", "chat.group_name_update_failed", "chat.group_icon_update_failed", "chat.typing_indicator.started", "chat.typing_indicator.stopped", "message.edited", "phone_number.status_updated" ], "doc_url": "https://docs.linqapp.com/guides/webhooks/events" } ``` --- # Webhook Subscriptions URL: https://docs.linqapp.com/api/resources/webhook_subscriptions/ ## Create a new webhook subscription **post** `/v3/webhook-subscriptions` Create a new webhook subscription to receive events at a target URL. Upon creation, a signing secret is generated for verifying webhook authenticity. **Store this secret securely — it cannot be retrieved later.** **Phone Number Filtering:** - Optionally specify `phone_numbers` to only receive events for specific lines - If omitted, events from all phone numbers are delivered (default behavior) - Use multiple subscriptions with different `phone_numbers` to route different lines to different endpoints - Each `target_url` can only be used once per account. To route different lines to different destinations, use a unique URL per subscription (e.g., append a query parameter: `https://example.com/webhook?line=1`) **Webhook Delivery:** - Events are sent via HTTP POST to the target URL - Each request includes [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`) for signature verification - Legacy `X-Webhook-*` headers are also sent for backwards compatibility (deprecated) - See [Verifying Webhook Signatures](https://docs.linqapp.com/guides/webhooks#verifying-webhook-signatures) for verification details - Failed deliveries (5xx, 429, network errors) are retried up to 10 times over ~25 minutes with exponential backoff - Client errors (4xx except 429) are not retried ### Body Parameters - `subscribed_events: array of WebhookEventType` List of event types to subscribe to - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent. Must be HTTPS. - `phone_numbers: optional array of string` Optional list of phone numbers to filter events for. Only events originating from these phone numbers will be delivered to this subscription. If omitted or empty, events from all phone numbers are delivered. Phone numbers must be in E.164 format. ### Returns - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `signing_secret: string` Secret for verifying webhook signatures. Store this securely - it cannot be retrieved again. - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "phone_numbers": [ "+12025551234", "+12025559876" ] }' ``` #### Response ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "signing_secret": "whsec_abc123def456", "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ``` ## List all webhook subscriptions **get** `/v3/webhook-subscriptions` Retrieve all webhook subscriptions for the authenticated partner. Returns a list of active and inactive subscriptions with their configuration and status. ### Returns - `subscriptions: array of WebhookSubscription` List of webhook subscriptions - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "subscriptions": [ { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ] } ``` ## Get a webhook subscription by ID **get** `/v3/webhook-subscriptions/{subscriptionId}` Retrieve details for a specific webhook subscription including its target URL, subscribed events, and current status. ### Path Parameters - `subscriptionId: string` ### Returns - `WebhookSubscription object { id, created_at, is_active, 4 more }` - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions/$SUBSCRIPTION_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ``` ## Update a webhook subscription **put** `/v3/webhook-subscriptions/{subscriptionId}` Update an existing webhook subscription. You can modify the target URL, subscribed events, or activate/deactivate the subscription. **Note:** The signing secret cannot be changed via this endpoint. ### Path Parameters - `subscriptionId: string` ### Body Parameters - `is_active: optional boolean` Activate or deactivate the subscription - `phone_numbers: optional array of string` Updated list of phone numbers to filter events for. Set to a non-empty array to filter events to specific phone numbers. Set to an empty array or null to remove the filter and receive events from all phone numbers. Phone numbers must be in E.164 format. - `subscribed_events: optional array of WebhookEventType` Updated list of event types to subscribe to - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: optional string` New target URL for webhook events ### Returns - `WebhookSubscription object { id, created_at, is_active, 4 more }` - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions/$SUBSCRIPTION_ID \ -X PUT \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "is_active": true, "phone_numbers": [ "+12025551234" ], "subscribed_events": [ "message.sent", "message.delivered" ], "target_url": "https://webhooks.example.com/linq/events" }' ``` #### Response ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ``` ## Delete a webhook subscription **delete** `/v3/webhook-subscriptions/{subscriptionId}` Delete a webhook subscription. ### Path Parameters - `subscriptionId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions/$SUBSCRIPTION_ID \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` ## Domain Types ### Webhook Subscription - `WebhookSubscription object { id, created_at, is_active, 4 more }` - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Webhook Subscription Create Response - `WebhookSubscriptionCreateResponse object { id, created_at, is_active, 5 more }` Response returned when creating a webhook subscription. Includes the signing secret which is only shown once. - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `signing_secret: string` Secret for verifying webhook signatures. Store this securely - it cannot be retrieved again. - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Webhook Subscription List Response - `WebhookSubscriptionListResponse object { subscriptions }` - `subscriptions: array of WebhookSubscription` List of webhook subscriptions - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. --- # api/resources/webhook_subscriptions/methods/create/index.md URL: https://docs.linqapp.com/api/resources/webhook_subscriptions/methods/create/ ## Create a new webhook subscription **post** `/v3/webhook-subscriptions` Create a new webhook subscription to receive events at a target URL. Upon creation, a signing secret is generated for verifying webhook authenticity. **Store this secret securely — it cannot be retrieved later.** **Phone Number Filtering:** - Optionally specify `phone_numbers` to only receive events for specific lines - If omitted, events from all phone numbers are delivered (default behavior) - Use multiple subscriptions with different `phone_numbers` to route different lines to different endpoints - Each `target_url` can only be used once per account. To route different lines to different destinations, use a unique URL per subscription (e.g., append a query parameter: `https://example.com/webhook?line=1`) **Webhook Delivery:** - Events are sent via HTTP POST to the target URL - Each request includes [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`) for signature verification - Legacy `X-Webhook-*` headers are also sent for backwards compatibility (deprecated) - See [Verifying Webhook Signatures](https://docs.linqapp.com/guides/webhooks#verifying-webhook-signatures) for verification details - Failed deliveries (5xx, 429, network errors) are retried up to 10 times over ~25 minutes with exponential backoff - Client errors (4xx except 429) are not retried ### Body Parameters - `subscribed_events: array of WebhookEventType` List of event types to subscribe to - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent. Must be HTTPS. - `phone_numbers: optional array of string` Optional list of phone numbers to filter events for. Only events originating from these phone numbers will be delivered to this subscription. If omitted or empty, events from all phone numbers are delivered. Phone numbers must be in E.164 format. ### Returns - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `signing_secret: string` Secret for verifying webhook signatures. Store this securely - it cannot be retrieved again. - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "phone_numbers": [ "+12025551234", "+12025559876" ] }' ``` #### Response ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "signing_secret": "whsec_abc123def456", "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ``` --- # api/resources/webhook_subscriptions/methods/delete/index.md URL: https://docs.linqapp.com/api/resources/webhook_subscriptions/methods/delete/ ## Delete a webhook subscription **delete** `/v3/webhook-subscriptions/{subscriptionId}` Delete a webhook subscription. ### Path Parameters - `subscriptionId: string` ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions/$SUBSCRIPTION_ID \ -X DELETE \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "error": { "status": 401, "code": 2004, "message": "Unauthorized - missing or invalid authentication token", "doc_url": "https://docs.linqapp.com/error/codes/2xxx/2004/" }, "success": false } ``` --- # api/resources/webhook_subscriptions/methods/list/index.md URL: https://docs.linqapp.com/api/resources/webhook_subscriptions/methods/list/ ## List all webhook subscriptions **get** `/v3/webhook-subscriptions` Retrieve all webhook subscriptions for the authenticated partner. Returns a list of active and inactive subscriptions with their configuration and status. ### Returns - `subscriptions: array of WebhookSubscription` List of webhook subscriptions - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "subscriptions": [ { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ] } ``` --- # api/resources/webhook_subscriptions/methods/retrieve/index.md URL: https://docs.linqapp.com/api/resources/webhook_subscriptions/methods/retrieve/ ## Get a webhook subscription by ID **get** `/v3/webhook-subscriptions/{subscriptionId}` Retrieve details for a specific webhook subscription including its target URL, subscribed events, and current status. ### Path Parameters - `subscriptionId: string` ### Returns - `WebhookSubscription object { id, created_at, is_active, 4 more }` - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions/$SUBSCRIPTION_ID \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" ``` #### Response ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ``` --- # api/resources/webhook_subscriptions/methods/update/index.md URL: https://docs.linqapp.com/api/resources/webhook_subscriptions/methods/update/ ## Update a webhook subscription **put** `/v3/webhook-subscriptions/{subscriptionId}` Update an existing webhook subscription. You can modify the target URL, subscribed events, or activate/deactivate the subscription. **Note:** The signing secret cannot be changed via this endpoint. ### Path Parameters - `subscriptionId: string` ### Body Parameters - `is_active: optional boolean` Activate or deactivate the subscription - `phone_numbers: optional array of string` Updated list of phone numbers to filter events for. Set to a non-empty array to filter events to specific phone numbers. Set to an empty array or null to remove the filter and receive events from all phone numbers. Phone numbers must be in E.164 format. - `subscribed_events: optional array of WebhookEventType` Updated list of event types to subscribe to - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: optional string` New target URL for webhook events ### Returns - `WebhookSubscription object { id, created_at, is_active, 4 more }` - `id: string` Unique identifier for the webhook subscription - `created_at: string` When the subscription was created - `is_active: boolean` Whether this subscription is currently active - `subscribed_events: array of WebhookEventType` List of event types this subscription receives - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `target_url: string` URL where webhook events will be sent - `updated_at: string` When the subscription was last updated - `phone_numbers: optional array of string` Phone numbers this subscription filters for. If null or empty, events from all phone numbers are delivered. ### Example ```http curl https://api.linqapp.com/api/partner/v3/webhook-subscriptions/$SUBSCRIPTION_ID \ -X PUT \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \ -d '{ "is_active": true, "phone_numbers": [ "+12025551234" ], "subscribed_events": [ "message.sent", "message.delivered" ], "target_url": "https://webhooks.example.com/linq/events" }' ``` #### Response ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2024-01-15T10:30:00Z", "is_active": true, "subscribed_events": [ "message.sent", "message.delivered", "message.read" ], "target_url": "https://webhooks.example.com/linq/events", "updated_at": "2024-01-15T10:30:00Z", "phone_numbers": [ "string" ] } ``` --- # Webhooks URL: https://docs.linqapp.com/api/resources/webhooks/ ## **** `` ## Domain Types ### Message Event V2 - `MessageEventV2 object { id, chat, direction, 10 more }` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat information - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: optional boolean` Whether this is a group chat - `owner_handle: optional ChatHandle` Your phone number's handle. Always has is_me=true. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `direction: "inbound" or "outbound"` Message direction - "outbound" if sent by you, "inbound" if received - `"inbound"` - `"outbound"` - `parts: array of SchemasTextPartResponse or SchemasMediaPartResponse or object { type, value } or object { app, layout, type, 2 more }` Message parts (text and/or media) - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `Link object { type, value }` A rich link preview part - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageApp object { app, layout, type, 2 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. - `name: string` Display name of the app. - `team_id: string` The app's 10-character team identifier. - `app_store_id: optional number` The owning app's App Store id, when known. - `layout: object { caption, subcaption, trailing_caption, trailing_subcaption }` Visible layout of the card. - `caption: optional string` Primary label, top-left and bold. - `subcaption: optional string` Secondary label, below caption on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below trailing_caption. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL the recipient's app opens when the user taps the card. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sender_handle: ChatHandle` The handle that sent this message - `service: ServiceType` Messaging service type - `delivered_at: optional string` When the message was delivered. Null if not yet delivered. - `effect: optional SchemasMessageEffect` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` - `idempotency_key: optional string` Idempotency key for deduplication of outbound messages. - `preferred_service: optional "iMessage" or "SMS" or "RCS" or "auto"` Preferred messaging service type. Includes "auto" for default fallback behavior. - `"iMessage"` - `"SMS"` - `"RCS"` - `"auto"` - `read_at: optional string` When the message was read. Null if not yet read. - `reply_to: optional object { message_id, part_index }` Reference to the message this is replying to (for threaded replies) - `message_id: optional string` ID of the message being replied to - `part_index: optional number` Index of the part being replied to - `sent_at: optional string` When the message was sent. Null if not yet sent. ### Message Payload - `MessagePayload object { id, created_at, delivered_at, 8 more }` Message content nested within webhook events - `id: optional string` Message identifier - `created_at: optional string` When the message record was created - `delivered_at: optional string` When the message was delivered - `effect: optional SchemasMessageEffect` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` - `is_delivered: optional boolean` Whether the message has been delivered - `is_read: optional boolean` Whether the message has been read - `parts: optional array of SchemasTextPartResponse or SchemasMediaPartResponse or object { type, value } or object { app, layout, type, 2 more }` Message content parts (text and/or media) - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `SchemasLinkPartResponse object { type, value }` A rich link preview part - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `SchemasIMessageAppPartResponse object { app, layout, type, 2 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. - `name: string` Display name of the app. - `team_id: string` The app's 10-character team identifier. - `app_store_id: optional number` The owning app's App Store id, when known. - `layout: object { caption, subcaption, trailing_caption, trailing_subcaption }` Visible layout of the card. - `caption: optional string` Primary label, top-left and bold. - `subcaption: optional string` Secondary label, below caption on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below trailing_caption. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL the recipient's app opens when the user taps the card. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `read_at: optional string` When the message was read - `reply_to: optional object { message_id, part_index }` Reference to the message this is replying to - `message_id: optional string` The ID of the message being replied to - `part_index: optional number` Index of the message part being replied to (0-based) - `sent_at: optional string` When the message was sent - `updated_at: optional string` When the message record was last updated ### Reaction Event Base - `ReactionEventBase object { is_from_me, reaction_type, chat_id, 8 more }` - `is_from_me: boolean` Whether this reaction was from the owner of the phone number (true) or from someone else (false) - `reaction_type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `chat_id: optional string` Chat identifier (UUID) - `custom_emoji: optional string` The actual emoji when reaction_type is "custom". Null for standard tapbacks. - `from: optional string` DEPRECATED: Use from_handle instead. Phone number or email address of the person who added/removed the reaction. - `from_handle: optional ChatHandle` The person who added/removed the reaction as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `message_id: optional string` Message identifier (UUID) that the reaction was added to or removed from - `part_index: optional number` Index of the message part that was reacted to (0-based) - `reacted_at: optional string` When the reaction was added or removed - `service: optional ServiceType` Messaging service type - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels ### Schemas Media Part Response - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). ### Schemas Message Effect - `SchemasMessageEffect object { name, type }` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` ### Schemas Text Part Response - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` ### Message Sent Webhook Event - `MessageSentWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.sent events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat information - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: optional boolean` Whether this is a group chat - `owner_handle: optional ChatHandle` Your phone number's handle. Always has is_me=true. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `direction: "inbound" or "outbound"` Message direction - "outbound" if sent by you, "inbound" if received - `"inbound"` - `"outbound"` - `parts: array of SchemasTextPartResponse or SchemasMediaPartResponse or object { type, value } or object { app, layout, type, 2 more }` Message parts (text and/or media) - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `Link object { type, value }` A rich link preview part - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageApp object { app, layout, type, 2 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. - `name: string` Display name of the app. - `team_id: string` The app's 10-character team identifier. - `app_store_id: optional number` The owning app's App Store id, when known. - `layout: object { caption, subcaption, trailing_caption, trailing_subcaption }` Visible layout of the card. - `caption: optional string` Primary label, top-left and bold. - `subcaption: optional string` Secondary label, below caption on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below trailing_caption. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL the recipient's app opens when the user taps the card. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sender_handle: ChatHandle` The handle that sent this message - `service: ServiceType` Messaging service type - `delivered_at: optional string` When the message was delivered. Null if not yet delivered. - `effect: optional SchemasMessageEffect` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` - `idempotency_key: optional string` Idempotency key for deduplication of outbound messages. - `preferred_service: optional "iMessage" or "SMS" or "RCS" or "auto"` Preferred messaging service type. Includes "auto" for default fallback behavior. - `"iMessage"` - `"SMS"` - `"RCS"` - `"auto"` - `read_at: optional string` When the message was read. Null if not yet read. - `reply_to: optional object { message_id, part_index }` Reference to the message this is replying to (for threaded replies) - `message_id: optional string` ID of the message being replied to - `part_index: optional number` Index of the part being replied to - `sent_at: optional string` When the message was sent. Null if not yet sent. - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Message Received Webhook Event - `MessageReceivedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.received events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat information - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: optional boolean` Whether this is a group chat - `owner_handle: optional ChatHandle` Your phone number's handle. Always has is_me=true. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `direction: "inbound" or "outbound"` Message direction - "outbound" if sent by you, "inbound" if received - `"inbound"` - `"outbound"` - `parts: array of SchemasTextPartResponse or SchemasMediaPartResponse or object { type, value } or object { app, layout, type, 2 more }` Message parts (text and/or media) - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `Link object { type, value }` A rich link preview part - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageApp object { app, layout, type, 2 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. - `name: string` Display name of the app. - `team_id: string` The app's 10-character team identifier. - `app_store_id: optional number` The owning app's App Store id, when known. - `layout: object { caption, subcaption, trailing_caption, trailing_subcaption }` Visible layout of the card. - `caption: optional string` Primary label, top-left and bold. - `subcaption: optional string` Secondary label, below caption on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below trailing_caption. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL the recipient's app opens when the user taps the card. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sender_handle: ChatHandle` The handle that sent this message - `service: ServiceType` Messaging service type - `delivered_at: optional string` When the message was delivered. Null if not yet delivered. - `effect: optional SchemasMessageEffect` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` - `idempotency_key: optional string` Idempotency key for deduplication of outbound messages. - `preferred_service: optional "iMessage" or "SMS" or "RCS" or "auto"` Preferred messaging service type. Includes "auto" for default fallback behavior. - `"iMessage"` - `"SMS"` - `"RCS"` - `"auto"` - `read_at: optional string` When the message was read. Null if not yet read. - `reply_to: optional object { message_id, part_index }` Reference to the message this is replying to (for threaded replies) - `message_id: optional string` ID of the message being replied to - `part_index: optional number` Index of the part being replied to - `sent_at: optional string` When the message was sent. Null if not yet sent. - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Message Read Webhook Event - `MessageReadWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.read events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat information - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: optional boolean` Whether this is a group chat - `owner_handle: optional ChatHandle` Your phone number's handle. Always has is_me=true. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `direction: "inbound" or "outbound"` Message direction - "outbound" if sent by you, "inbound" if received - `"inbound"` - `"outbound"` - `parts: array of SchemasTextPartResponse or SchemasMediaPartResponse or object { type, value } or object { app, layout, type, 2 more }` Message parts (text and/or media) - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `Link object { type, value }` A rich link preview part - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageApp object { app, layout, type, 2 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. - `name: string` Display name of the app. - `team_id: string` The app's 10-character team identifier. - `app_store_id: optional number` The owning app's App Store id, when known. - `layout: object { caption, subcaption, trailing_caption, trailing_subcaption }` Visible layout of the card. - `caption: optional string` Primary label, top-left and bold. - `subcaption: optional string` Secondary label, below caption on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below trailing_caption. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL the recipient's app opens when the user taps the card. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sender_handle: ChatHandle` The handle that sent this message - `service: ServiceType` Messaging service type - `delivered_at: optional string` When the message was delivered. Null if not yet delivered. - `effect: optional SchemasMessageEffect` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` - `idempotency_key: optional string` Idempotency key for deduplication of outbound messages. - `preferred_service: optional "iMessage" or "SMS" or "RCS" or "auto"` Preferred messaging service type. Includes "auto" for default fallback behavior. - `"iMessage"` - `"SMS"` - `"RCS"` - `"auto"` - `read_at: optional string` When the message was read. Null if not yet read. - `reply_to: optional object { message_id, part_index }` Reference to the message this is replying to (for threaded replies) - `message_id: optional string` ID of the message being replied to - `part_index: optional number` Index of the part being replied to - `sent_at: optional string` When the message was sent. Null if not yet sent. - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Message Delivered Webhook Event - `MessageDeliveredWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.delivered events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat information - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: optional boolean` Whether this is a group chat - `owner_handle: optional ChatHandle` Your phone number's handle. Always has is_me=true. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `direction: "inbound" or "outbound"` Message direction - "outbound" if sent by you, "inbound" if received - `"inbound"` - `"outbound"` - `parts: array of SchemasTextPartResponse or SchemasMediaPartResponse or object { type, value } or object { app, layout, type, 2 more }` Message parts (text and/or media) - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `Link object { type, value }` A rich link preview part - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageApp object { app, layout, type, 2 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. - `name: string` Display name of the app. - `team_id: string` The app's 10-character team identifier. - `app_store_id: optional number` The owning app's App Store id, when known. - `layout: object { caption, subcaption, trailing_caption, trailing_subcaption }` Visible layout of the card. - `caption: optional string` Primary label, top-left and bold. - `subcaption: optional string` Secondary label, below caption on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below trailing_caption. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL the recipient's app opens when the user taps the card. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sender_handle: ChatHandle` The handle that sent this message - `service: ServiceType` Messaging service type - `delivered_at: optional string` When the message was delivered. Null if not yet delivered. - `effect: optional SchemasMessageEffect` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` - `idempotency_key: optional string` Idempotency key for deduplication of outbound messages. - `preferred_service: optional "iMessage" or "SMS" or "RCS" or "auto"` Preferred messaging service type. Includes "auto" for default fallback behavior. - `"iMessage"` - `"SMS"` - `"RCS"` - `"auto"` - `read_at: optional string` When the message was read. Null if not yet read. - `reply_to: optional object { message_id, part_index }` Reference to the message this is replying to (for threaded replies) - `message_id: optional string` ID of the message being replied to - `part_index: optional number` Index of the part being replied to - `sent_at: optional string` When the message was sent. Null if not yet sent. - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Message Failed Webhook Event - `MessageFailedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.failed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { code, failed_at, chat_id, 2 more }` Error details for message.failed webhook events. See [WebhookErrorCode](#/components/schemas/WebhookErrorCode) for the full error code reference. - `code: number` Error codes in webhook failure events (3007, 4001, 4005). - `failed_at: string` When the failure was detected - `chat_id: optional string` Chat identifier (UUID) - `message_id: optional string` Message identifier (UUID) - `reason: optional string` Human-readable description of the failure - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Message Edited Webhook Event - `MessageEditedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.edited events (2026-02-03 format only) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { id, chat, direction, 3 more }` Payload for `message.edited` events (2026-02-03 format). Describes which part of a message was edited and when. Only text parts can be edited. Only available for subscriptions using `webhook_version: "2026-02-03"`. - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat context - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: boolean` Whether this is a group chat - `owner_handle: ChatHandle` The handle that owns this chat (your phone number) - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `direction: "outbound" or "inbound"` "outbound" if you sent the original message, "inbound" if you received it - `"outbound"` - `"inbound"` - `edited_at: string` When the edit occurred - `part: object { index, text }` The edited part - `index: number` Zero-based index of the edited part within the message - `text: string` New text content of the part - `sender_handle: ChatHandle` The handle that sent (and edited) this message - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Reaction Added Webhook Event - `ReactionAddedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for reaction.added events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: ReactionEventBase` Payload for reaction.added webhook events - `is_from_me: boolean` Whether this reaction was from the owner of the phone number (true) or from someone else (false) - `reaction_type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `chat_id: optional string` Chat identifier (UUID) - `custom_emoji: optional string` The actual emoji when reaction_type is "custom". Null for standard tapbacks. - `from: optional string` DEPRECATED: Use from_handle instead. Phone number or email address of the person who added/removed the reaction. - `from_handle: optional ChatHandle` The person who added/removed the reaction as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `message_id: optional string` Message identifier (UUID) that the reaction was added to or removed from - `part_index: optional number` Index of the message part that was reacted to (0-based) - `reacted_at: optional string` When the reaction was added or removed - `service: optional ServiceType` Messaging service type - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Reaction Removed Webhook Event - `ReactionRemovedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for reaction.removed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: ReactionEventBase` Payload for reaction.removed webhook events - `is_from_me: boolean` Whether this reaction was from the owner of the phone number (true) or from someone else (false) - `reaction_type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `chat_id: optional string` Chat identifier (UUID) - `custom_emoji: optional string` The actual emoji when reaction_type is "custom". Null for standard tapbacks. - `from: optional string` DEPRECATED: Use from_handle instead. Phone number or email address of the person who added/removed the reaction. - `from_handle: optional ChatHandle` The person who added/removed the reaction as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `message_id: optional string` Message identifier (UUID) that the reaction was added to or removed from - `part_index: optional number` Index of the message part that was reacted to (0-based) - `reacted_at: optional string` When the reaction was added or removed - `service: optional ServiceType` Messaging service type - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Participant Added Webhook Event - `ParticipantAddedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for participant.added events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { handle, added_at, chat_id, participant }` Payload for participant.added webhook events - `handle: string` DEPRECATED: Use participant instead. Handle (phone number or email address) of the added participant. - `added_at: optional string` When the participant was added - `chat_id: optional string` Chat identifier (UUID) of the group chat - `participant: optional ChatHandle` The added participant as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Participant Removed Webhook Event - `ParticipantRemovedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for participant.removed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { handle, chat_id, participant, removed_at }` Payload for participant.removed webhook events - `handle: string` DEPRECATED: Use participant instead. Handle (phone number or email address) of the removed participant. - `chat_id: optional string` Chat identifier (UUID) of the group chat - `participant: optional ChatHandle` The removed participant as a full handle object - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `removed_at: optional string` When the participant was removed - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Chat Created Webhook Event - `ChatCreatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.created events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { id, created_at, display_name, 5 more }` Payload for chat.created webhook events. Matches GET /v3/chats/{chatId} response. - `id: string` Unique identifier for the chat - `created_at: string` When the chat was created - `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` List of chat participants with full handle details. Always contains at least two handles (your phone number and the other participant). - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: boolean` Whether this is a group chat - `updated_at: string` When the chat was last updated - `service: optional ServiceType` Messaging service type - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Chat Group Name Updated Webhook Event - `ChatGroupNameUpdatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_name_updated events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, updated_at, changed_by_handle, 2 more }` Payload for chat.group_name_updated webhook events - `chat_id: string` Chat identifier (UUID) of the group chat - `updated_at: string` When the update occurred - `changed_by_handle: optional ChatHandle` The handle who made the change. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `new_value: optional string` New group name (null if the name was removed) - `old_value: optional string` Previous group name (null if no previous name) - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Chat Group Icon Updated Webhook Event - `ChatGroupIconUpdatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_icon_updated events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, updated_at, changed_by_handle, 2 more }` Payload for chat.group_icon_updated webhook events - `chat_id: string` Chat identifier (UUID) of the group chat - `updated_at: string` When the update occurred - `changed_by_handle: optional ChatHandle` The handle who made the change. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `new_value: optional string` New icon URL (null if the icon was removed) - `old_value: optional string` Previous icon URL (null if no previous icon) - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Chat Group Name Update Failed Webhook Event - `ChatGroupNameUpdateFailedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_name_update_failed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, error_code, failed_at }` Error details for chat.group_name_update_failed webhook events. See [WebhookErrorCode](#/components/schemas/WebhookErrorCode) for the full error code reference. - `chat_id: string` Chat identifier (UUID) of the group chat - `error_code: number` Error codes in webhook failure events (3007, 4001, 4005). - `failed_at: string` When the failure was detected - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Chat Group Icon Update Failed Webhook Event - `ChatGroupIconUpdateFailedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_icon_update_failed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, error_code, failed_at }` Error details for chat.group_icon_update_failed webhook events. See [WebhookErrorCode](#/components/schemas/WebhookErrorCode) for the full error code reference. - `chat_id: string` Chat identifier (UUID) of the group chat - `error_code: number` Error codes in webhook failure events (3007, 4001, 4005). - `failed_at: string` When the failure was detected - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Chat Typing Indicator Started Webhook Event - `ChatTypingIndicatorStartedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.typing_indicator.started events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id }` Payload for chat.typing_indicator.started webhook events - `chat_id: string` Chat identifier - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Chat Typing Indicator Stopped Webhook Event - `ChatTypingIndicatorStoppedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.typing_indicator.stopped events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id }` Payload for chat.typing_indicator.stopped webhook events - `chat_id: string` Chat identifier - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Phone Number Status Updated Webhook Event - `PhoneNumberStatusUpdatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for phone_number.status_updated events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { changed_at, new_health_status, new_reputation, 5 more }` Payload for phone_number.status_updated webhook events - `changed_at: string` When the status change occurred - `new_health_status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `new_reputation: "HEALTHY" or "AT_RISK" or "CRITICAL"` The new line reputation - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `new_status: "ACTIVE" or "FLAGGED"` The new service status - `"ACTIVE"` - `"FLAGGED"` - `phone_number: string` Phone number in E.164 format - `previous_health_status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `previous_reputation: "HEALTHY" or "AT_RISK" or "CRITICAL"` The previous line reputation - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `previous_status: "ACTIVE" or "FLAGGED"` The previous service status - `"ACTIVE"` - `"FLAGGED"` - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: "message.sent" or "message.received" or "message.read" or 24 more` The type of event - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. ### Unwrap Webhook Event - `UnwrapWebhookEvent = MessageSentWebhookEvent or MessageReceivedWebhookEvent or MessageReadWebhookEvent or 15 more` Complete webhook payload for message.sent events (2026-02-03 format) - `MessageSentWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.sent events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat information - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: optional boolean` Whether this is a group chat - `owner_handle: optional ChatHandle` Your phone number's handle. Always has is_me=true. - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `"iMessage"` - `"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) - `status: optional "active" or "left" or "removed"` Participant status - `"active"` - `"left"` - `"removed"` - `direction: "inbound" or "outbound"` Message direction - "outbound" if sent by you, "inbound" if received - `"inbound"` - `"outbound"` - `parts: array of SchemasTextPartResponse or SchemasMediaPartResponse or object { type, value } or object { app, layout, type, 2 more }` Message parts (text and/or media) - `SchemasTextPartResponse object { type, value, text_decorations }` A text message part - `type: "text"` Indicates this is a text message part - `"text"` - `value: string` The text content - `text_decorations: optional array of TextDecoration` Text decorations applied to character ranges in the value - `range: array of number` Character range `[start, end)` in the `value` string where the decoration applies. `start` is inclusive, `end` is exclusive. *Characters are measured as UTF-16 code units. Most characters count as 1; some emoji count as 2.* - `animation: optional "big" or "small" or "shake" or 5 more` Animated text effect to apply. Mutually exclusive with `style`. - `"big"` - `"small"` - `"shake"` - `"nod"` - `"explode"` - `"ripple"` - `"bloom"` - `"jitter"` - `style: optional "bold" or "italic" or "strikethrough" or "underline"` Text style to apply. Mutually exclusive with `animation`. - `"bold"` - `"italic"` - `"strikethrough"` - `"underline"` - `SchemasMediaPartResponse object { id, filename, mime_type, 3 more }` A media attachment part - `id: string` Unique attachment identifier - `filename: string` Original filename - `mime_type: string` MIME type of the file - `size_bytes: number` File size in bytes - `type: "media"` Indicates this is a media attachment part - `"media"` - `url: string` Presigned URL for downloading the attachment (expires in 1 hour). - `Link object { type, value }` A rich link preview part - `type: "link"` Indicates this is a rich link preview part - `"link"` - `value: string` The URL - `IMessageApp object { app, layout, type, 2 more }` An iMessage app card part. - `app: object { bundle_id, name, team_id, app_store_id }` Identifies the iMessage app (Messages app extension) that backs the card. - `bundle_id: string` Bundle identifier of the Messages app extension. - `name: string` Display name of the app. - `team_id: string` The app's 10-character team identifier. - `app_store_id: optional number` The owning app's App Store id, when known. - `layout: object { caption, subcaption, trailing_caption, trailing_subcaption }` Visible layout of the card. - `caption: optional string` Primary label, top-left and bold. - `subcaption: optional string` Secondary label, below caption on the left. - `trailing_caption: optional string` Label shown top-right. - `trailing_subcaption: optional string` Label shown below trailing_caption. - `type: "imessage_app"` Indicates this is an iMessage app card part. - `"imessage_app"` - `url: string` The URL the recipient's app opens when the user taps the card. - `fallback_text: optional string` Fallback text for surfaces that cannot render the card. - `sender_handle: ChatHandle` The handle that sent this message - `service: ServiceType` Messaging service type - `delivered_at: optional string` When the message was delivered. Null if not yet delivered. - `effect: optional SchemasMessageEffect` iMessage effect applied to a message (screen or bubble animation) - `name: optional string` Effect name (confetti, fireworks, slam, gentle, etc.) - `type: optional "screen" or "bubble"` Effect category - `"screen"` - `"bubble"` - `idempotency_key: optional string` Idempotency key for deduplication of outbound messages. - `preferred_service: optional "iMessage" or "SMS" or "RCS" or "auto"` Preferred messaging service type. Includes "auto" for default fallback behavior. - `"iMessage"` - `"SMS"` - `"RCS"` - `"auto"` - `read_at: optional string` When the message was read. Null if not yet read. - `reply_to: optional object { message_id, part_index }` Reference to the message this is replying to (for threaded replies) - `message_id: optional string` ID of the message being replied to - `part_index: optional number` Index of the part being replied to - `sent_at: optional string` When the message was sent. Null if not yet sent. - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `MessageReceivedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.received events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `MessageReadWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.read events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `MessageDeliveredWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.delivered events (2026-02-03 format) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: MessageEventV2` Unified payload for message webhooks when using `webhook_version: "2026-02-03"`. This schema is used for message.sent, message.received, message.delivered, and message.read events when the subscription URL includes `?version=2026-02-03`. Key differences from V1 (2025-01-01): - `direction`: "inbound" or "outbound" instead of `is_from_me` boolean - `sender_handle`: Full handle object for the sender - `chat`: Nested object with `id`, `is_group`, and `owner_handle` - Message fields (`id`, `parts`, `effect`, etc.) are at the top level, not nested in `message` Timestamps indicate the message state: - `message.sent`: sent_at set, delivered_at=null, read_at=null - `message.received`: sent_at set, delivered_at=null, read_at=null - `message.delivered`: sent_at set, delivered_at set, read_at=null - `message.read`: sent_at set, delivered_at set, read_at set - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `MessageFailedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.failed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { code, failed_at, chat_id, 2 more }` Error details for message.failed webhook events. See [WebhookErrorCode](#/components/schemas/WebhookErrorCode) for the full error code reference. - `code: number` Error codes in webhook failure events (3007, 4001, 4005). - `failed_at: string` When the failure was detected - `chat_id: optional string` Chat identifier (UUID) - `message_id: optional string` Message identifier (UUID) - `reason: optional string` Human-readable description of the failure - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `MessageEditedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for message.edited events (2026-02-03 format only) - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { id, chat, direction, 3 more }` Payload for `message.edited` events (2026-02-03 format). Describes which part of a message was edited and when. Only text parts can be edited. Only available for subscriptions using `webhook_version: "2026-02-03"`. - `id: string` Message identifier - `chat: object { id, health_status, is_group, owner_handle }` Chat context - `id: string` Chat identifier - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: boolean` Whether this is a group chat - `owner_handle: ChatHandle` The handle that owns this chat (your phone number) - `direction: "outbound" or "inbound"` "outbound" if you sent the original message, "inbound" if you received it - `"outbound"` - `"inbound"` - `edited_at: string` When the edit occurred - `part: object { index, text }` The edited part - `index: number` Zero-based index of the edited part within the message - `text: string` New text content of the part - `sender_handle: ChatHandle` The handle that sent (and edited) this message - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ReactionAddedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for reaction.added events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: ReactionEventBase` Payload for reaction.added webhook events - `is_from_me: boolean` Whether this reaction was from the owner of the phone number (true) or from someone else (false) - `reaction_type: ReactionType` Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, emphasize, question. Custom emoji reactions have type "custom" with the actual emoji in the custom_emoji field. Sticker reactions have type "sticker" with sticker attachment details in the sticker field. - `"love"` - `"like"` - `"dislike"` - `"laugh"` - `"emphasize"` - `"question"` - `"custom"` - `"sticker"` - `chat_id: optional string` Chat identifier (UUID) - `custom_emoji: optional string` The actual emoji when reaction_type is "custom". Null for standard tapbacks. - `from: optional string` DEPRECATED: Use from_handle instead. Phone number or email address of the person who added/removed the reaction. - `from_handle: optional ChatHandle` The person who added/removed the reaction as a full handle object - `message_id: optional string` Message identifier (UUID) that the reaction was added to or removed from - `part_index: optional number` Index of the message part that was reacted to (0-based) - `reacted_at: optional string` When the reaction was added or removed - `service: optional ServiceType` Messaging service type - `sticker: optional object { file_name, height, mime_type, 2 more }` Sticker attachment details when reaction_type is "sticker". Null for non-sticker reactions. - `file_name: optional string` Filename of the sticker - `height: optional number` Sticker image height in pixels - `mime_type: optional string` MIME type of the sticker image - `url: optional string` Presigned URL for downloading the sticker image (expires in 1 hour). - `width: optional number` Sticker image width in pixels - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ReactionRemovedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for reaction.removed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: ReactionEventBase` Payload for reaction.removed webhook events - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ParticipantAddedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for participant.added events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { handle, added_at, chat_id, participant }` Payload for participant.added webhook events - `handle: string` DEPRECATED: Use participant instead. Handle (phone number or email address) of the added participant. - `added_at: optional string` When the participant was added - `chat_id: optional string` Chat identifier (UUID) of the group chat - `participant: optional ChatHandle` The added participant as a full handle object - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ParticipantRemovedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for participant.removed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { handle, chat_id, participant, removed_at }` Payload for participant.removed webhook events - `handle: string` DEPRECATED: Use participant instead. Handle (phone number or email address) of the removed participant. - `chat_id: optional string` Chat identifier (UUID) of the group chat - `participant: optional ChatHandle` The removed participant as a full handle object - `removed_at: optional string` When the participant was removed - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ChatCreatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.created events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { id, created_at, display_name, 5 more }` Payload for chat.created webhook events. Matches GET /v3/chats/{chatId} response. - `id: string` Unique identifier for the chat - `created_at: string` When the chat was created - `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` List of chat participants with full handle details. Always contains at least two handles (your phone number and the other participant). - `id: string` Unique identifier for this handle - `handle: string` Phone number (E.164) or email address of the participant - `joined_at: string` When this participant joined the chat - `service: ServiceType` Messaging service type - `is_me: optional boolean` Whether this handle belongs to the sender (your phone number) - `left_at: optional string` When they left (if applicable) - `status: optional "active" or "left" or "removed"` Participant status - `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 gate sends or surface 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. See the [Chat Health guide](/guides/chats/chat-health) 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. - `status: "HEALTHY" or "AT_RISK" or "CRITICAL" or "OPTED_OUT"` Current health bucket for the chat. See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `"OPTED_OUT"` - `updated_at: string` When this status last changed. - `is_group: boolean` Whether this is a group chat - `updated_at: string` When the chat was last updated - `service: optional ServiceType` Messaging service type - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ChatGroupNameUpdatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_name_updated events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, updated_at, changed_by_handle, 2 more }` Payload for chat.group_name_updated webhook events - `chat_id: string` Chat identifier (UUID) of the group chat - `updated_at: string` When the update occurred - `changed_by_handle: optional ChatHandle` The handle who made the change. - `new_value: optional string` New group name (null if the name was removed) - `old_value: optional string` Previous group name (null if no previous name) - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ChatGroupIconUpdatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_icon_updated events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, updated_at, changed_by_handle, 2 more }` Payload for chat.group_icon_updated webhook events - `chat_id: string` Chat identifier (UUID) of the group chat - `updated_at: string` When the update occurred - `changed_by_handle: optional ChatHandle` The handle who made the change. - `new_value: optional string` New icon URL (null if the icon was removed) - `old_value: optional string` Previous icon URL (null if no previous icon) - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ChatGroupNameUpdateFailedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_name_update_failed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, error_code, failed_at }` Error details for chat.group_name_update_failed webhook events. See [WebhookErrorCode](#/components/schemas/WebhookErrorCode) for the full error code reference. - `chat_id: string` Chat identifier (UUID) of the group chat - `error_code: number` Error codes in webhook failure events (3007, 4001, 4005). - `failed_at: string` When the failure was detected - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ChatGroupIconUpdateFailedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.group_icon_update_failed events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id, error_code, failed_at }` Error details for chat.group_icon_update_failed webhook events. See [WebhookErrorCode](#/components/schemas/WebhookErrorCode) for the full error code reference. - `chat_id: string` Chat identifier (UUID) of the group chat - `error_code: number` Error codes in webhook failure events (3007, 4001, 4005). - `failed_at: string` When the failure was detected - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ChatTypingIndicatorStartedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.typing_indicator.started events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id }` Payload for chat.typing_indicator.started webhook events - `chat_id: string` Chat identifier - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `ChatTypingIndicatorStoppedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for chat.typing_indicator.stopped events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { chat_id }` Payload for chat.typing_indicator.stopped webhook events - `chat_id: string` Chat identifier - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: WebhookEventType` Valid webhook event types that can be subscribed to. **Note:** `message.edited` is only delivered to subscriptions using `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025 subscription will not produce any deliveries. - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. - `PhoneNumberStatusUpdatedWebhookEvent object { api_version, created_at, data, 5 more }` Complete webhook payload for phone_number.status_updated events - `api_version: string` API version for the webhook payload format - `created_at: string` When the event was created - `data: object { changed_at, new_health_status, new_reputation, 5 more }` Payload for phone_number.status_updated webhook events - `changed_at: string` When the status change occurred - `new_health_status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `new_reputation: "HEALTHY" or "AT_RISK" or "CRITICAL"` The new line reputation - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `new_status: "ACTIVE" or "FLAGGED"` The new service status - `"ACTIVE"` - `"FLAGGED"` - `phone_number: string` Phone number in E.164 format - `previous_health_status: "HEALTHY" or "AT_RISK" or "CRITICAL"` Current reputation of this phone line as assessed by risk-service. - `HEALTHY` — No elevated risk detected. - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or reviewing messaging patterns. - `CRITICAL` — High risk; further sending may result in line flagging or restriction. Defaults to `HEALTHY` for lines that have not yet been scored. - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `previous_reputation: "HEALTHY" or "AT_RISK" or "CRITICAL"` The previous line reputation - `"HEALTHY"` - `"AT_RISK"` - `"CRITICAL"` - `previous_status: "ACTIVE" or "FLAGGED"` The previous service status - `"ACTIVE"` - `"FLAGGED"` - `event_id: string` Unique identifier for this event (for deduplication) - `event_type: "message.sent" or "message.received" or "message.read" or 24 more` The type of event - `"message.sent"` - `"message.received"` - `"message.read"` - `"message.delivered"` - `"message.failed"` - `"message.edited"` - `"reaction.added"` - `"reaction.removed"` - `"participant.added"` - `"participant.removed"` - `"chat.created"` - `"chat.group_name_updated"` - `"chat.group_icon_updated"` - `"chat.group_name_update_failed"` - `"chat.group_icon_update_failed"` - `"chat.typing_indicator.started"` - `"chat.typing_indicator.stopped"` - `"phone_number.status_updated"` - `"call.initiated"` - `"call.ringing"` - `"call.answered"` - `"call.ended"` - `"call.failed"` - `"call.declined"` - `"call.no_answer"` - `"location.sharing.started"` - `"location.sharing.stopped"` - `partner_id: string` Partner identifier. Present on all webhooks for cross-referencing. - `trace_id: string` Trace ID for debugging and correlation across systems. - `webhook_version: string` Date-based webhook payload version. Determined by the `?version=` query parameter in your webhook subscription URL. If no version parameter is specified, defaults based on subscription creation date. --- # api/resources/webhooks/methods/unwrap/index.md URL: https://docs.linqapp.com/api/resources/webhooks/methods/unwrap/ ## **** `` --- # Overview URL: https://docs.linqapp.com/v2/api/ ## Linq Partner API 2.0.0 The Linq Partner API enables you to send and receive iMessages, RCS, and SMS at scale through your organization’s phone numbers. Build powerful messaging experiences, automate conversations, and integrate Linq’s messaging infrastructure directly into your applications. Messages are automatically sent using the best available protocol: iMessage, then RCS, then SMS. ## Getting Started All API requests use `https://api.linqapp.com` as the base URL and require authentication via the `X-LINQ-INTEGRATION-TOKEN` header. Your integration token determines which phone numbers you can message from and which organization data you can access. ``` curl https://api.linqapp.com/api/partner/v2/chats \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" ``` ## Key Concepts - **Chats**: Conversation threads with one or more participants - **Messages**: Individual messages within a chat, supporting text, attachments, and reactions - **Phone Numbers**: Your organization’s messaging-enabled phone numbers (iMessage, RCS, and SMS) - **Contacts**: People in your organization’s address book - **Webhooks**: Real-time notifications for incoming messages, reactions, and events Linq Support - <support@linqapp.com> Informations - OpenAPI version: `3.0.3` ## Operations GET [/api/partner/v2/chats](/v2/api/operations/apipartnerv2chats/get/index.md) POST [/api/partner/v2/chats](/v2/api/operations/apipartnerv2chats/post/index.md) GET [/api/partner/v2/chats/{chat\_id}](/v2/api/operations/apipartnerv2chatschat_id/index.md) GET [/api/partner/v2/chats/find](/v2/api/operations/apipartnerv2chatsfind/index.md) PUT [/api/partner/v2/chats/{chat\_id}/mark\_as\_read](/v2/api/operations/apipartnerv2chatschat_idmark_as_read/index.md) POST [/api/partner/v2/chats/{chat\_id}/start\_typing](/v2/api/operations/apipartnerv2chatschat_idstart_typing/index.md) DELETE [/api/partner/v2/chats/{chat\_id}/stop\_typing](/v2/api/operations/apipartnerv2chatschat_idstop_typing/index.md) POST [/api/partner/v2/chats/{chat\_id}/share\_contact](/v2/api/operations/apipartnerv2chatschat_idshare_contact/index.md) GET [/api/partner/v2/chats/{chat\_id}/chat\_messages](/v2/api/operations/apipartnerv2chatschat_idchat_messages/get/index.md) POST [/api/partner/v2/chats/{chat\_id}/chat\_messages](/v2/api/operations/apipartnerv2chatschat_idchat_messages/post/index.md) GET [/api/partner/v2/chats/{chat\_id}/chat\_messages/{id}](/v2/api/operations/apipartnerv2chatschat_idchat_messagesid/get/index.md) DELETE [/api/partner/v2/chats/{chat\_id}/chat\_messages/{id}](/v2/api/operations/apipartnerv2chatschat_idchat_messagesid/delete/index.md) POST [/api/partner/v2/chats/{chat\_id}/chat\_messages/{id}/edit](/v2/api/operations/apipartnerv2chatschat_idchat_messagesidedit/index.md) POST [/api/partner/v2/chat\_messages/{chat\_message\_id}/reactions](/v2/api/operations/apipartnerv2chat_messageschat_message_idreactions/index.md) GET [/api/partner/v2/chat\_message\_reactions/{reaction\_id}](/v2/api/operations/apipartnerv2chat_message_reactionsreaction_id/index.md) GET [/api/partner/v2/contacts](/v2/api/operations/apipartnerv2contacts/get/index.md) POST [/api/partner/v2/contacts](/v2/api/operations/apipartnerv2contacts/post/index.md) GET [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/get/index.md) PUT [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/put/index.md) DELETE [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/delete/index.md) PATCH [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/patch/index.md) GET [/api/partner/v2/contacts/find](/v2/api/operations/apipartnerv2contactsfind/index.md) GET [/api/partner/v2/phone\_numbers](/v2/api/operations/apipartnerv2phone_numbers/index.md) PUT [/api/partner/v2/phone\_numbers/{id}](/v2/api/operations/apipartnerv2phone_numbersid/index.md) GET [/api/partner/v2/webhook\_subscriptions](/v2/api/operations/apipartnerv2webhook_subscriptions/get/index.md) POST [/api/partner/v2/webhook\_subscriptions](/v2/api/operations/apipartnerv2webhook_subscriptions/post/index.md) PUT [/api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id}](/v2/api/operations/apipartnerv2webhook_subscriptionswebhook_subscription_id/put/index.md) DELETE [/api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id}](/v2/api/operations/apipartnerv2webhook_subscriptionswebhook_subscription_id/delete/index.md) GET [/webhooks/events](/v2/api/operations/webhooksevents/index.md) POST [/api/partner/v2/i\_message\_availability/check](/v2/api/operations/apipartnerv2i_message_availabilitycheck/index.md) ## Authentication ### ApiKeyAuth **Security scheme type: **apiKey **Header parameter name: **X-LINQ-INTEGRATION-TOKEN --- # Get Chat Message Reaction URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chat_message_reactionsreaction_id/ GET /api/partner/v2/chat\_message\_reactions/{reaction\_id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chat_message_reactions/1'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://api.linqapp.com/api/partner/v2/chat_message_reactions/1 \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chat\_message\_reactions/{reaction\_id} Retrieves details for a specific reaction ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **reaction\_id** required integer The reaction ID ## Responses ### 200 Successful response Media type application/json object **data** object **id** integer **chat\_message\_id** integer **reaction** The type of reaction string Allowed values: love like dislike laugh emphasize question **is\_from\_me** boolean **from\_phone** string **sent\_at** string format: date-time **created\_at** string format: date-time **updated\_at** string format: date-time ##### Example ``` { "data": { "id": 456, "chat_message_id": 224, "reaction": "love", "is_from_me": false, "from_phone": "+15551234567", "sent_at": "2025-05-21T15:30:00.000-05:00", "created_at": "2025-05-21T15:30:00.000-05:00", "updated_at": "2025-05-21T15:30:00.000-05:00" } } ``` --- # React to Message URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chat_messageschat_message_idreactions/ POST /api/partner/v2/chat\_messages/{chat\_message\_id}/reactions Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chat_messages/1/reactions'; const options = { method: 'POST', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"type":"love","operation":"add"}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/chat_messages/1/reactions \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "type": "love", "operation": "add" }' ``` - Production server api.linqapp.com/api/partner/v2/chat\_messages/{chat\_message\_id}/reactions Adds or removes a reaction from a message ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_message\_id** required integer The message ID ## Request Body required Media type application/json object **type** required The type of reaction string Allowed values: love like dislike laugh emphasize question ##### Example ``` love ``` **operation** required Whether to add or remove the reaction string Allowed values: add remove ##### Example ``` add ``` ## Responses ### 201 Reaction added successfully Media type application/json object **data** object **id** integer **chat\_message\_id** integer **reaction** The type of reaction string Allowed values: love like dislike laugh emphasize question **is\_from\_me** boolean **from\_phone** string **sent\_at** string format: date-time **created\_at** string format: date-time **updated\_at** string format: date-time ##### Example ``` { "data": { "id": 456, "chat_message_id": 224, "reaction": "love", "is_from_me": false, "from_phone": "+15551234567", "sent_at": "2025-05-21T15:30:00.000-05:00", "created_at": "2025-05-21T15:30:00.000-05:00", "updated_at": "2025-05-21T15:30:00.000-05:00" } } ``` --- # List Chats URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chats/get/ GET /api/partner/v2/chats Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats?phone_number=13343284472&page=1&per_page=25'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://api.linqapp.com/api/partner/v2/chats?phone_number=13343284472&page=1&per_page=25' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats Retrieves a paginated list of chats for the authenticated partner filtered by phone number. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Query Parameters **phone\_number** required string ##### Example ``` 13343284472 ``` Phone number to filter chats by. Returns all chats involving this phone number. **page** integer default: 1 Page number for pagination (default 1) **per\_page** integer default: 25 <= 100 Number of items per page (default 25, max 100) ## Responses ### 200 Successful response Media type application/json object **data** Array\<object> object **id** integer **display\_name** The display name for the chat. Returns a manually set group name if present, otherwise a comma-separated list of participant names/phone numbers. string **service** The messaging service used for this chat string Allowed values: iMessage SMS RCS **group** Whether this is a group chat boolean **message\_count** Number of messages in the chat. For newly created chats, this will be 1 (the initial message). For existing chats, this could be any number. integer **chat\_handles** Array\<object> object **id** integer **phone\_number** The phone number or email identifier for this participant string **service** The messaging service for this handle string Allowed values: iMessage SMS RCS **joined\_at** string format: date-time **meta** object **page** integer **total\_pages** integer **total\_count** integer **per\_page** integer ##### Example ``` { "data": [ { "id": 45, "display_name": "John Doe, Jane Smith", "service": "iMessage", "group": false, "message_count": 25, "chat_handles": [ { "id": 123, "phone_number": "+15551234567", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" }, { "id": 124, "phone_number": "+15559876543", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" } ] } ], "meta": { "page": 1, "total_pages": 5, "total_count": 123, "per_page": 25 } } ``` ### 401 Unauthorized Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 401, "code": "unauthorized", "title": "Unauthorized", "detail": "Invalid or missing authentication token" } ] } ``` --- # Create Chat URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chats/post/ POST /api/partner/v2/chats Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats'; const options = { method: 'POST', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"send_from":"+15175269229","chat":{"display_name":"Sample Chat","phone_numbers":["+13343284472","+13344713465"]},"message":{"text":"Hello! This is a sample message from Linq API v2.","idempotency_key":"msg-2024_01-23_abc123"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/chats \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "send_from": "+15175269229", "chat": { "display_name": "Sample Chat", "phone_numbers": [ "+13343284472", "+13344713465" ] }, "message": { "text": "Hello! This is a sample message from Linq API v2.", "idempotency_key": "msg-2024_01-23_abc123" } }' ``` - Production server api.linqapp.com/api/partner/v2/chats Creates a new chat with the specified phone numbers, or returns an existing chat if one already exists with the same participants. At least one phone number must be provided. **Note:** This endpoint uses “find or create” logic: - If a chat already exists with these participants, returns the existing chat (which may have many messages) - If no chat exists, creates a new chat with your initial message (will have `message_count: 1`) ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Request Body required Media type application/json object **send\_from** required Phone number to send from. This must be one of your organization’s Linq phone numbers. string **chat** required object **display\_name** Display name for the chat (optional) string **phone\_numbers** required Phone numbers to include in the chat Array\<string> \>= 1 items **message** required Initial message to send when creating the chat object **text** required Message text content string **idempotency\_key** Optional unique key to prevent duplicate messages. Must be alphanumeric and may include hyphens and underscores. If a message with this key was already sent, returns the existing message. string /^\[a-zA-Z0-9\_-]+$/ **attachments** Optional file attachments to upload with the message. Files are uploaded as part of the multipart/form-data request. Include one field per file. Omit this field if not attaching files. Array\<string> **attachment\_urls** Optional URLs of files to attach to the message. The API will download files from these URLs (must be publicly accessible). Uses a 30-second timeout per download. Can be used together with direct file uploads via `attachments`. Omit this field if not attaching files via URLs. Array\<string> ##### Examples Select example basic\_message Basic message without attachments ``` { "send_from": "+15175269229", "chat": { "display_name": "Sample Chat", "phone_numbers": [ "+13343284472", "+13344713465" ] }, "message": { "text": "Hello! This is a sample message from Linq API v2.", "idempotency_key": "msg-2024_01-23_abc123" } } ``` Message with attachment URLs ``` { "send_from": "+15175269229", "chat": { "phone_numbers": [ "+13343284472" ] }, "message": { "text": "Here are the documents you requested.", "attachment_urls": [ "https://example.com/document.pdf", "https://example.com/image.jpg" ] } } ``` Message with direct file uploads For direct file uploads, use multipart/form-data. The attachments field accepts binary file data. Note: This is a simplified example. In actual implementation, use multipart/form-data with binary file data. ``` { "send_from": "+15175269229", "chat": { "phone_numbers": [ "+13343284472" ] }, "message": { "text": "Sending files directly", "attachments": [ "<binary file data>" ] } } ``` ## Responses ### 200 Chat created successfully (or existing chat returned if already exists) Media type application/json object **data** object **id** integer **display\_name** string **service** string Allowed values: iMessage SMS RCS **group** boolean **chat\_handles** Array\<object> object **id** integer **phone\_number** The phone number or email identifier for this participant string **service** The messaging service for this handle string Allowed values: iMessage SMS RCS **joined\_at** string format: date-time **chat\_messages** The initial message that was sent when creating the chat object **id** integer **text** string **sent\_at** string format: date-time **delivered\_at** Timestamp when message was delivered. Initially null when message is pending, populated once delivered. string format: date-time nullable **delivery\_status** Current delivery status. Starts as “pending”, changes to “delivered” once successfully delivered. string Allowed values: pending delivered rate\_limit\_exceeded paused **is\_read** boolean **attachments** Array of attachments if any were included Array\<object> object **id** Unique identifier for the attachment string format: uuid **url** string **filename** string **mime\_type** string **file\_size** integer ##### Example ``` { "data": { "id": 45, "display_name": "John Doe", "service": "iMessage", "group": false, "chat_handles": [ { "id": 123, "phone_number": "+15551234567", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" }, { "id": 124, "phone_number": "+15559876543", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" } ], "chat_messages": { "id": 16470817, "text": "Hello! This is a sample message from Linq API v2.", "sent_at": "2025-10-23T13:07:55.019-05:00", "delivered_at": null, "delivery_status": "pending", "is_read": false, "attachments": [ { "id": "abc12345-1234-5678-9abc-def012345678", "url": "https://storage.googleapis.com/linq-files/attachments/abc123.pdf", "filename": "document.pdf", "mime_type": "application/pdf", "file_size": 12345 } ] } } } ``` ### 400 Bad request - Invalid or missing parameters (client-side error). Please verify all required parameters are included and properly formatted. Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 400, "code": "missing_phone", "title": "Missing Phone", "detail": "Missing required parameter: send_from" } ] } ``` ### 422 Validation error Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 422, "code": "invalid_params", "title": "Invalid Params", "detail": "Phone number is required" } ] } ``` --- # Get Chat URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_id/ GET /api/partner/v2/chats/{chat\_id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://api.linqapp.com/api/partner/v2/chats/1 \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id} Retrieves details for a specific chat ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID ## Responses ### 200 Successful response Media type application/json object **data** object **id** integer **display\_name** The display name for the chat. Returns a manually set group name if present, otherwise a comma-separated list of participant names/phone numbers. string **service** The messaging service used for this chat string Allowed values: iMessage SMS RCS **group** Whether this is a group chat boolean **message\_count** Number of messages in the chat. For newly created chats, this will be 1 (the initial message). For existing chats, this could be any number. integer **chat\_handles** Array\<object> object **id** integer **phone\_number** The phone number or email identifier for this participant string **service** The messaging service for this handle string Allowed values: iMessage SMS RCS **joined\_at** string format: date-time ##### Example ``` { "data": { "id": 45, "display_name": "John Doe, Jane Smith", "service": "iMessage", "group": false, "message_count": 15, "chat_handles": [ { "id": 123, "phone_number": "+15551234567", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" }, { "id": 124, "phone_number": "+15559876543", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" } ] } } ``` ### 404 Chat not found Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 404, "code": "not_found", "title": "Not Found", "detail": "Chat not found" } ] } ``` --- # List Chat Messages URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idchat_messages/get/ GET /api/partner/v2/chats/{chat\_id}/chat\_messages Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/chat_messages?page=1&per_page=25&before=2025-05-21T15%3A30%3A00.000-05%3A00&after=2025-05-21T15%3A30%3A00.000-05%3A00&sort=asc'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://api.linqapp.com/api/partner/v2/chats/1/chat_messages?page=1&per_page=25&before=2025-05-21T15%3A30%3A00.000-05%3A00&after=2025-05-21T15%3A30%3A00.000-05%3A00&sort=asc' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/chat\_messages Retrieves a paginated list of messages for a specific chat ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID ### Query Parameters **page** integer default: 1 Page number for pagination (default 1) **per\_page** integer default: 25 <= 100 Number of items per page (default 25, max 100) **before** string format: date-time ##### Example ``` 2025-05-21T15:30:00.000-05:00 ``` Filter messages sent before this timestamp (ISO 8601 format) **after** string format: date-time ##### Example ``` 2025-05-21T15:30:00.000-05:00 ``` Filter messages sent after this timestamp (ISO 8601 format) **sort** string default: asc Allowed values: asc desc Sort order for messages (default is chronological/ascending) ## Responses ### 200 Successful response Media type application/json object **data** Array\<object> object **id** integer **text** string **sent\_at** string format: date-time **delivered\_at** string format: date-time nullable **delivery\_status** Current delivery status of the message string Allowed values: pending delivered service\_unavailable paused **edited\_at** string format: date-time nullable **is\_read** boolean **sent\_from** The phone number or identifier that sent this message string **chat\_handle\_id** integer **attachments** Array\<object> object **id** Unique identifier for the attachment string format: uuid **url** string **filename** string **mime\_type** string **file\_size** integer **reactions** Array\<object> object **id** integer **chat\_message\_id** integer **reaction** The type of reaction string Allowed values: love like dislike laugh emphasize question **is\_from\_me** boolean **from\_phone** string **sent\_at** string format: date-time **created\_at** string format: date-time **updated\_at** string format: date-time **meta** object **page** integer **total\_pages** integer **total\_count** integer **per\_page** integer ##### Example ``` { "data": [ { "id": 224, "text": "Hello, how are you?", "sent_at": "2025-05-21T15:30:00.123-05:00", "delivered_at": "2025-05-21T15:30:05.456-05:00", "delivery_status": "pending", "edited_at": "2025-05-21T15:31:00.789-05:00", "is_read": false, "sent_from": "+15551234567", "chat_handle_id": 123, "attachments": [ { "id": "abc12345-1234-5678-9abc-def012345678", "url": "https://storage.googleapis.com/linq-files/attachments/abc123.pdf", "filename": "document.pdf", "mime_type": "application/pdf", "file_size": 12345 } ], "reactions": [ { "id": 456, "chat_message_id": 224, "reaction": "love", "is_from_me": false, "from_phone": "+15551234567", "sent_at": "2025-05-21T15:30:00.000-05:00", "created_at": "2025-05-21T15:30:00.000-05:00", "updated_at": "2025-05-21T15:30:00.000-05:00" } ] } ], "meta": { "page": 1, "total_pages": 5, "total_count": 123, "per_page": 25 } } ``` --- # Send Message URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idchat_messages/post/ POST /api/partner/v2/chats/{chat\_id}/chat\_messages Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/chat_messages'; const form = new FormData(); form.append('message[text]', 'Hello, how are you?'); form.append('message[idempotency_key]', 'msg-2024_01-23_abc123'); const options = { method: 'POST', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; options.body = form; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/chats/1/chat_messages \ --header 'Content-Type: multipart/form-data' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --form 'message[text]=Hello, how are you?' \ --form 'message[idempotency_key]=msg-2024_01-23_abc123' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/chat\_messages Sends a new message in the specified chat. **Idempotency:** To prevent duplicate messages (e.g., due to network retries), include a unique `message[idempotency_key]` in your request. If a message with the same key was already sent, the API will return the existing message with a 200 status instead of creating a duplicate. The idempotency key must be alphanumeric and may include hyphens and underscores. **Attachments:** You can attach files in two ways: 1. **Direct upload** - Use `message[attachments][]` fields in your multipart/form-data request to upload files directly: ``` -F "message[attachments][]=@file1.jpg" \ -F "message[attachments][]=@file2.png" ``` 2. **URLs** - Use `message[attachment_urls][]` to provide URLs of files. The API will download them from the provided URLs (must be publicly accessible, 30-second timeout per download): ``` -F "message[attachment_urls][]=https://example.com/document.pdf" \ -F "message[attachment_urls][]=https://example.com/image.jpg" ``` Both methods can be used together in the same message. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID ## Request Body required Media type multipart/form-data object **message\[text]** required The message text content string **message\[idempotency\_key]** Optional unique key to prevent duplicate messages. Must be alphanumeric and may include hyphens and underscores. string /^\[a-zA-Z0-9\_-]+$/ **message\[attachments]\[]** Optional file attachments to upload. Include one field per file (e.g., message\[attachments]\[]=@file1.jpg message\[attachments]\[]=@file2.png). Omit this field if not uploading files. Array\<string> **message\[attachment\_urls]\[]** Optional URLs of files to attach. The API will download files from these URLs (must be publicly accessible). Uses a 30-second timeout per download. Omit this field if not attaching files via URLs. Array\<string> ##### Examples Select example basic\_message Basic text message ``` { "message[text]": "Hello, how are you?", "message[idempotency_key]": "msg-2024_01-23_abc123" } ``` Message with attachment URLs ``` { "message[text]": "Here are the files you requested.", "message[attachment_urls][]": [ "https://example.com/document.pdf", "https://example.com/image.jpg" ] } ``` Message with direct file uploads For direct file uploads via multipart/form-data. Use cURL with -F flags: curl -X POST “[https://api.linqapp.com/api/partner/v2/chats/{chat\_id}/chat\_messages](https://api.linqapp.com/api/partner/v2/chats/%7Bchat_id%7D/chat_messages)”\ -H “X-LINQ-INTEGRATION-TOKEN: your\_token”\ -F “message\[text]=Check out these files”\ -F “message\[attachments]\[]=@/path/to/file1.jpg”\ -F “message\[attachments]\[]=@/path/to/file2.pdf” ``` { "message[text]": "Check out these files", "message[attachments][]": [ "<binary file data>" ] } ``` ## Responses ### 201 Message sent successfully Media type application/json object **data** object **id** integer **text** string **sent\_at** string format: date-time **delivered\_at** string format: date-time nullable **delivery\_status** Current delivery status of the message string Allowed values: pending delivered service\_unavailable paused **edited\_at** string format: date-time nullable **is\_read** boolean **sent\_from** The phone number or identifier that sent this message string **chat\_handle\_id** integer **attachments** Array\<object> object **id** Unique identifier for the attachment string format: uuid **url** string **filename** string **mime\_type** string **file\_size** integer **reactions** Array\<object> object **id** integer **chat\_message\_id** integer **reaction** The type of reaction string Allowed values: love like dislike laugh emphasize question **is\_from\_me** boolean **from\_phone** string **sent\_at** string format: date-time **created\_at** string format: date-time **updated\_at** string format: date-time ##### Example ``` { "data": { "id": 224, "text": "Hello, how are you?", "sent_at": "2025-05-21T15:30:00.123-05:00", "delivered_at": "2025-05-21T15:30:05.456-05:00", "delivery_status": "pending", "edited_at": "2025-05-21T15:31:00.789-05:00", "is_read": false, "sent_from": "+15551234567", "chat_handle_id": 123, "attachments": [ { "id": "abc12345-1234-5678-9abc-def012345678", "url": "https://storage.googleapis.com/linq-files/attachments/abc123.pdf", "filename": "document.pdf", "mime_type": "application/pdf", "file_size": 12345 } ], "reactions": [ { "id": 456, "chat_message_id": 224, "reaction": "love", "is_from_me": false, "from_phone": "+15551234567", "sent_at": "2025-05-21T15:30:00.000-05:00", "created_at": "2025-05-21T15:30:00.000-05:00", "updated_at": "2025-05-21T15:30:00.000-05:00" } ] } } ``` --- # Delete Message URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idchat_messagesid/delete/ DELETE /api/partner/v2/chats/{chat\_id}/chat\_messages/{id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/chat_messages/1'; const options = { method: 'DELETE', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request DELETE \ --url https://api.linqapp.com/api/partner/v2/chats/1/chat_messages/1 \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/chat\_messages/{id} Deletes a message from the chat ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID **id** required integer The message ID ## Responses ### 200 Message deleted successfully Media type application/json object **message** string ##### Example ``` { "message": "Chat message deleted successfully" } ``` --- # Get Chat Message URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idchat_messagesid/get/ GET /api/partner/v2/chats/{chat\_id}/chat\_messages/{id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/chat_messages/1'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://api.linqapp.com/api/partner/v2/chats/1/chat_messages/1 \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/chat\_messages/{id} Retrieves details for a specific message ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID **id** required integer The message ID ## Responses ### 200 Successful response Media type application/json object **data** object **id** integer **text** string **sent\_at** string format: date-time **delivered\_at** string format: date-time nullable **delivery\_status** Current delivery status of the message string Allowed values: pending delivered service\_unavailable paused **edited\_at** string format: date-time nullable **is\_read** boolean **sent\_from** The phone number or identifier that sent this message string **chat\_handle\_id** integer **attachments** Array\<object> object **id** Unique identifier for the attachment string format: uuid **url** string **filename** string **mime\_type** string **file\_size** integer **reactions** Array\<object> object **id** integer **chat\_message\_id** integer **reaction** The type of reaction string Allowed values: love like dislike laugh emphasize question **is\_from\_me** boolean **from\_phone** string **sent\_at** string format: date-time **created\_at** string format: date-time **updated\_at** string format: date-time ##### Example ``` { "data": { "id": 224, "text": "Hello, how are you?", "sent_at": "2025-05-21T15:30:00.123-05:00", "delivered_at": "2025-05-21T15:30:05.456-05:00", "delivery_status": "pending", "edited_at": "2025-05-21T15:31:00.789-05:00", "is_read": false, "sent_from": "+15551234567", "chat_handle_id": 123, "attachments": [ { "id": "abc12345-1234-5678-9abc-def012345678", "url": "https://storage.googleapis.com/linq-files/attachments/abc123.pdf", "filename": "document.pdf", "mime_type": "application/pdf", "file_size": 12345 } ], "reactions": [ { "id": 456, "chat_message_id": 224, "reaction": "love", "is_from_me": false, "from_phone": "+15551234567", "sent_at": "2025-05-21T15:30:00.000-05:00", "created_at": "2025-05-21T15:30:00.000-05:00", "updated_at": "2025-05-21T15:30:00.000-05:00" } ] } } ``` --- # Edit Message URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idchat_messagesidedit/ POST /api/partner/v2/chats/{chat\_id}/chat\_messages/{id}/edit Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/chat_messages/1/edit'; const options = { method: 'POST', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"text":"Updated message text"}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/chats/1/chat_messages/1/edit \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "text": "Updated message text" }' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/chat\_messages/{id}/edit Edits an existing message in the chat ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID **id** required integer The message ID ## Request Body required Media type application/json object **text** required string ##### Example ``` Updated message text ``` ## Responses ### 200 Message edited successfully Media type application/json object **message** string **data** object **id** integer **text** string ##### Example ``` { "message": "Message edited successfully", "data": { "id": 243062, "text": "nice" } } ``` --- # Mark Chat as Read URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idmark_as_read/ PUT /api/partner/v2/chats/{chat\_id}/mark\_as\_read Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/mark_as_read'; const options = { method: 'PUT', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PUT \ --url https://api.linqapp.com/api/partner/v2/chats/1/mark_as_read \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/mark\_as\_read Marks all messages in a chat as read. This endpoint does not require a request body. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID ## Responses ### 204 Chat marked as read successfully ### 404 Chat not found Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 404, "code": "not_found", "title": "Not Found", "detail": "Chat not found" } ] } ``` --- # Share Contact Card URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idshare_contact/ POST /api/partner/v2/chats/{chat\_id}/share\_contact Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/share_contact'; const options = { method: 'POST', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/chats/1/share_contact \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/share\_contact Shares your Linq contact card (name and image associated with your Linq number) with the participants in the chat. This endpoint does not require a request body. **Note:** Contact cards must be enabled for your users by the Linq team. Contact your Linq representative to enable this feature. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID ## Responses ### 201 Contact card shared successfully ### 404 Chat not found Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 404, "code": "not_found", "title": "Not Found", "detail": "Chat not found" } ] } ``` --- # Start Typing Indicator URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idstart_typing/ POST /api/partner/v2/chats/{chat\_id}/start\_typing Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/start_typing'; const options = { method: 'POST', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/chats/1/start_typing \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/start\_typing Sends a typing indicator to show that you are currently typing in the chat. This endpoint does not require a request body. The typing indicator has a default timeout of 60 seconds. Note that sending a message will automatically terminate the typing indicator without needing to call the DELETE endpoint. **Note:** By default, the Linq platform automatically displays typing indicators to make message responses appear more human-like. If you want to manually control typing indicators using this API, contact your Linq representative to disable automated typing indicators for your organization. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID ## Responses ### 201 Typing indicator started successfully ### 404 Chat not found Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 404, "code": "not_found", "title": "Not Found", "detail": "Chat not found" } ] } ``` --- # Stop Typing Indicator URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatschat_idstop_typing/ DELETE /api/partner/v2/chats/{chat\_id}/stop\_typing Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/1/stop_typing'; const options = { method: 'DELETE', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request DELETE \ --url https://api.linqapp.com/api/partner/v2/chats/1/stop_typing \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/{chat\_id}/stop\_typing Removes the typing indicator to show that you have stopped typing in the chat. This endpoint does not require a request body. **Note:** By default, the Linq platform automatically manages typing indicators. If you want to manually control typing indicators using this API, contact your Linq representative to disable automated typing indicators for your organization. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **chat\_id** required integer The chat ID ## Responses ### 204 Typing indicator stopped successfully ### 404 Chat not found Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 404, "code": "not_found", "title": "Not Found", "detail": "Chat not found" } ] } ``` --- # Find Chat URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2chatsfind/ GET /api/partner/v2/chats/find Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/chats/find?phone_number=%2B12052761148&phone_numbers%5B%5D=%2B12055426109'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://api.linqapp.com/api/partner/v2/chats/find?phone_number=%2B12052761148&phone_numbers%5B%5D=%2B12055426109' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/chats/find Finds a chat by phone numbers. Specify your Linq number via ‘phone\_number’ and the participant(s) you want to find a chat with via ‘phone\_numbers\[]’. Returns the first matching chat. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Query Parameters **phone\_number** required string ##### Example ``` +12052761148 ``` Your Linq number (the phone number you’re searching from) **phone\_numbers\[]** required Array\<string> ##### Example ``` [ "+12055426109" ] ``` Array of phone numbers to find a chat with. Can be a single number or multiple numbers to find a group chat. ## Responses ### 200 Successful response Media type application/json object **data** One of: - [object](#tab-panel-0) - [object](#tab-panel-1) When chat is found object **chat** object **id** integer **display\_name** The display name for the chat. Returns a manually set group name if present, otherwise a comma-separated list of participant names/phone numbers. string **service** The messaging service used for this chat string Allowed values: iMessage SMS RCS **group** Whether this is a group chat boolean **message\_count** Number of messages in the chat. For newly created chats, this will be 1 (the initial message). For existing chats, this could be any number. integer **chat\_handles** Array\<object> object **id** integer **phone\_number** The phone number or email identifier for this participant string **service** The messaging service for this handle string Allowed values: iMessage SMS RCS **joined\_at** string format: date-time When chat is not found object **chat** null nullable **messages** Array\<object> object ##### Examples Select example chat\_found Chat found ``` { "data": { "chat": { "id": 45, "display_name": "John Doe", "service": "iMessage", "group": false, "message_count": 15, "chat_handles": [ { "id": 123, "phone_number": "+15551234567", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" }, { "id": 124, "phone_number": "+15559876543", "service": "iMessage", "joined_at": "2025-05-21T15:30:00.000-05:00" } ] } } } ``` Chat not found ``` { "data": { "chat": null, "messages": [] } } ``` --- # List Contacts URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2contacts/get/ GET /api/partner/v2/contacts Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/contacts?page=1&per_page=25'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://api.linqapp.com/api/partner/v2/contacts?page=1&per_page=25' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/contacts Retrieves a paginated list of all contacts in your organization ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Query Parameters **page** integer default: 1 Page number for pagination (default 1) **per\_page** integer default: 25 <= 100 Number of items per page (default 25, max 100) ## Responses ### 200 Successful response Media type application/json object **contacts** Array\<object> object **id** integer **first\_name** string **last\_name** string **full\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **image\_url** string nullable **created\_at** string format: date-time **updated\_at** string format: date-time **contact\_owner** The user who owns this contact in your organization object **id** integer **email** string **first\_name** string **last\_name** string **name** string **pagination** object **current\_page** integer **per\_page** integer **total\_pages** integer **total\_count** integer ##### Example ``` { "contacts": [ { "id": 123, "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA", "image_url": null, "created_at": "2025-07-30T10:00:00.000-05:00", "updated_at": "2025-07-30T10:00:00.000-05:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } }, { "id": 124, "first_name": "Jane", "last_name": "Smith", "full_name": "Jane Smith", "email": "jane@example.com", "phone_number": "+15559876543", "company": "Tech Corp", "title": "CTO", "location": "New York, NY", "image_url": null, "created_at": "2025-07-29T10:00:00.000-05:00", "updated_at": "2025-07-29T10:00:00.000-05:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } } ], "pagination": { "current_page": 1, "per_page": 25, "total_pages": 5, "total_count": 123 } } ``` ### 500 Internal server error Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Example ``` { "status": "error", "error_code": "INTERNAL_ERROR", "message": "Unable to fetch contacts", "errors": [ "Internal server error. Please try again or contact support if the issue persists." ] } ``` --- # Create Contact URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2contacts/post/ POST /api/partner/v2/contacts Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/contacts'; const options = { method: 'POST', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"contact":{"first_name":"John","last_name":"Doe","email":"john@example.com","phone_number":"+15551234567","company":"Acme Corp","title":"CEO","location":"San Francisco, CA"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/contacts \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "contact": { "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA" } }' ``` - Production server api.linqapp.com/api/partner/v2/contacts Creates a new contact. You can optionally specify a user\_email to assign the contact to a specific user in your organization. If not provided, the contact will be automatically associated with the first admin user. **Note:** At least one of the following fields must be provided: `first_name`, `last_name`, `email`, or `phone_number`. Attempting to create a contact without any of these fields will result in a 422 validation error. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Request Body required Media type application/json object **contact** required object **first\_name** string **last\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **user\_email** Optional email of the user in your organization who should own this contact. Must be an existing user in your organization. If not provided, defaults to the first admin user. string ##### Examples Select example withoutOwner Create contact with default owner ``` { "contact": { "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA" } } ``` Create contact with specific owner ``` { "contact": { "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA" }, "user_email": "owner@company.com" } ``` ## Responses ### 201 Contact created successfully Media type application/json object **data** object **id** integer **first\_name** string **last\_name** string **full\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **image\_url** string nullable **created\_at** string format: date-time **updated\_at** string format: date-time **contact\_owner** The user who owns this contact in your organization object **id** integer **email** string **first\_name** string **last\_name** string **name** string ##### Example ``` { "data": { "id": 123, "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA", "image_url": null, "created_at": "2025-07-30T10:00:00.000-05:00", "updated_at": "2025-07-30T10:00:00.000-05:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } } } ``` ### 422 Validation error or User not found Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Examples Select example validation\_error Validation error ``` { "status": "error", "error_code": "VALIDATION_ERROR", "message": "Contact validation failed", "errors": [ "At least one of the name, email, or phone number fields must be filled out for the contact" ] } ``` User not found ``` { "status": "error", "error_code": "USER_NOT_FOUND", "message": "Specified user not found", "errors": [ "No user with email 'owner@company.com' exists in your organization" ] } ``` --- # Find Contact URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2contactsfind/ GET /api/partner/v2/contacts/find Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/contacts/find?email=john%40example.com&phone_number=%2B15551234567'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://api.linqapp.com/api/partner/v2/contacts/find?email=john%40example.com&phone_number=%2B15551234567' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/contacts/find Finds a contact by email or phone number. At least one search parameter must be provided. If both email and phone\_number are provided, the search uses OR logic (returns the contact if either matches). **Phone Number Handling:** Phone numbers are automatically normalized to E.164 format before searching. You can provide phone numbers in various formats (e.g., “(555) 123-4567”, “555-123-4567”, “+15551234567”) and they will be normalized for matching. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Query Parameters **email** string format: email Email address to search for ##### Example ``` john@example.com ``` **phone\_number** string Phone number to search for. The API will automatically normalize it to E.164 format for searching (e.g., “+15551234567”). ##### Example ``` +15551234567 ``` ## Responses ### 200 Successful response (returns contact if found, or null if not found) Media type application/json One of: - [object](#tab-panel-2) - [object](#tab-panel-3) object **id** integer **first\_name** string **last\_name** string **full\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **image\_url** string nullable **created\_at** string format: date-time **updated\_at** string format: date-time **contact\_owner** The user who owns this contact in your organization object **id** integer **email** string **first\_name** string **last\_name** string **name** string object **contact** null ##### Examples Select example contact\_found Contact found ``` { "id": 123, "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA", "image_url": null, "created_at": "2025-07-30T10:00:00.000-05:00", "updated_at": "2025-07-30T10:00:00.000-05:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } } ``` Contact not found ``` { "contact": null } ``` ### 400 Bad request - Missing search parameters (client-side error). Please verify all required parameters are included and properly formatted. Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Example ``` { "status": "error", "error_code": "MISSING_SEARCH_PARAMS", "message": "At least one search parameter is required", "errors": [ "Please provide either email or phone_number" ] } ``` ### 422 Validation error - Invalid email or phone format Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Examples Select example invalid\_email Invalid email format ``` { "status": "error", "error_code": "INVALID_EMAIL_FORMAT", "message": "Invalid email format", "errors": [ "The provided email address is not in a valid format" ] } ``` Invalid phone format ``` { "status": "error", "error_code": "INVALID_PHONE_FORMAT", "message": "Invalid phone number format", "errors": [ "The provided phone number is not in a valid format. Please use E.164 format (e.g., +15551234567)" ] } ``` ### 500 Internal server error Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Example ``` { "status": "error", "error_code": "INTERNAL_ERROR", "message": "Unable to find contact", "errors": [ "Internal server error. Please try again or contact support if the issue persists." ] } ``` --- # Delete Contact URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2contactsid/delete/ DELETE /api/partner/v2/contacts/{id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/contacts/1'; const options = { method: 'DELETE', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request DELETE \ --url https://api.linqapp.com/api/partner/v2/contacts/1 \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/contacts/{id} Deletes a contact and all associated user contacts. This action cannot be undone. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **id** required integer The contact ID ## Responses ### 204 Contact deleted successfully ### 404 Contact not found Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Example ``` { "status": "error", "error_code": "CONTACT_NOT_FOUND", "message": "Contact not found", "errors": [ "The requested contact does not exist or is not accessible to your organization" ] } ``` --- # Get Contact URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2contactsid/get/ GET /api/partner/v2/contacts/{id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/contacts/1'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://api.linqapp.com/api/partner/v2/contacts/1 \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/contacts/{id} Retrieves details for a specific contact ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **id** required integer The contact ID ## Responses ### 200 Successful response Media type application/json object **data** object **id** integer **first\_name** string **last\_name** string **full\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **image\_url** string nullable **created\_at** string format: date-time **updated\_at** string format: date-time **contact\_owner** The user who owns this contact in your organization object **id** integer **email** string **first\_name** string **last\_name** string **name** string ##### Example ``` { "data": { "id": 123, "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA", "image_url": null, "created_at": "2025-07-30T10:00:00.000-05:00", "updated_at": "2025-07-30T10:00:00.000-05:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } } } ``` ### 404 Contact not found Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Example ``` { "status": "error", "error_code": "CONTACT_NOT_FOUND", "message": "Contact not found", "errors": [ "The requested contact does not exist or is not accessible to your organization" ] } ``` --- # Partially Update Contact URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2contactsid/patch/ PATCH /api/partner/v2/contacts/{id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/contacts/1'; const options = { method: 'PATCH', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"contact":{"email":"newemail@example.com"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PATCH \ --url https://api.linqapp.com/api/partner/v2/contacts/1 \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "contact": { "email": "newemail@example.com" } }' ``` - Production server api.linqapp.com/api/partner/v2/contacts/{id} Partially updates an existing contact. Only the fields provided will be updated. Functionally identical to PUT. **At least one of the following fields must be provided:** - `first_name` - `last_name` - `email` - `phone_number` **Additional optional fields:** - `company` - `title` - `location` - `user_email` (to change contact owner) ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **id** required integer The contact ID ## Request Body required Media type application/json object **contact** required object **first\_name** string **last\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **user\_email** Optional email of the user in your organization who should own this contact. Must be an existing user in your organization. If not provided, defaults to the first admin user. string ##### Examples Select example update\_email\_only Update email only ``` { "contact": { "email": "newemail@example.com" } } ``` Update multiple fields ``` { "contact": { "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA" }, "user_email": "owner@company.com" } ``` ## Responses ### 200 Contact updated successfully Media type application/json object **data** object **id** integer **first\_name** string **last\_name** string **full\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **image\_url** string nullable **created\_at** string format: date-time **updated\_at** string format: date-time **contact\_owner** The user who owns this contact in your organization object **id** integer **email** string **first\_name** string **last\_name** string **name** string ##### Example ``` { "data": { "id": 123, "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA", "image_url": null, "created_at": "2025-07-30T10:00:00.000-05:00", "updated_at": "2025-07-30T10:00:00.000-05:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } } } ``` ### 404 Contact not found Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Example ``` { "status": "error", "error_code": "CONTACT_NOT_FOUND", "message": "Contact not found", "errors": [ "The requested contact does not exist or is not accessible to your organization" ] } ``` --- # Update Contact URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2contactsid/put/ PUT /api/partner/v2/contacts/{id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/contacts/1'; const options = { method: 'PUT', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"contact":{"email":"newemail@example.com"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PUT \ --url https://api.linqapp.com/api/partner/v2/contacts/1 \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "contact": { "email": "newemail@example.com" } }' ``` - Production server api.linqapp.com/api/partner/v2/contacts/{id} Updates an existing contact. Only the fields provided will be updated. **At least one of the following fields must be provided:** - `first_name` - `last_name` - `email` - `phone_number` **Additional optional fields:** - `company` - `title` - `location` - `user_email` (to change contact owner) ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **id** required integer The contact ID ## Request Body required Media type application/json object **contact** required object **first\_name** string **last\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **user\_email** Optional email of the user in your organization who should own this contact. Must be an existing user in your organization. If not provided, defaults to the first admin user. string ##### Examples Select example update\_email\_only Update email only ``` { "contact": { "email": "newemail@example.com" } } ``` Update multiple fields ``` { "contact": { "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA" }, "user_email": "owner@company.com" } ``` ## Responses ### 200 Contact updated successfully Media type application/json object **data** object **id** integer **first\_name** string **last\_name** string **full\_name** string **email** string **phone\_number** string **company** string **title** string **location** string **image\_url** string nullable **created\_at** string format: date-time **updated\_at** string format: date-time **contact\_owner** The user who owns this contact in your organization object **id** integer **email** string **first\_name** string **last\_name** string **name** string ##### Example ``` { "data": { "id": 123, "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA", "image_url": null, "created_at": "2025-07-30T10:00:00.000-05:00", "updated_at": "2025-07-30T10:00:00.000-05:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } } } ``` ### 404 Contact not found Media type application/json Alternative error format used by Contacts and Webhook Subscriptions endpoints (render\_standard\_error format) object **status** string **error\_code** string **message** string **errors** Array\<string> ##### Example ``` { "status": "error", "error_code": "CONTACT_NOT_FOUND", "message": "Contact not found", "errors": [ "The requested contact does not exist or is not accessible to your organization" ] } ``` --- # Check iMessage Availability URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2i_message_availabilitycheck/ POST /api/partner/v2/i\_message\_availability/check Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/i_message_availability/check'; const options = { method: 'POST', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"phone_number":"+15551234567"}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/i_message_availability/check \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "phone_number": "+15551234567" }' ``` - Production server api.linqapp.com/api/partner/v2/i\_message\_availability/check Checks if a phone number is registered with iMessage. **Rate Limit:** This endpoint is limited to 1 request per 10 seconds. Exceeding this limit will result in a `429` error response. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Request Body required Media type application/json object **phone\_number** required string ##### Example ``` +15551234567 ``` ## Responses ### 200 Successful response Media type application/json object **available** boolean **phone\_number** string ##### Example ``` { "available": true, "phone_number": "+15551234567" } ``` --- # List Phone Numbers URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2phone_numbers/ GET /api/partner/v2/phone\_numbers Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/phone_numbers'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://api.linqapp.com/api/partner/v2/phone_numbers \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/phone\_numbers Retrieves all phone numbers associated with the authenticated partner organization ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Responses ### 200 Successful response Media type application/json object **phone\_numbers** Array\<object> object **id** integer **phone\_number** string **forwarding\_number** Phone number where calls are forwarded when this number is unavailable string nullable **response\_rate** Response rate as messages per second integer ##### Example ``` { "phone_numbers": [ { "id": 99, "phone_number": "+19498151221", "forwarding_number": null, "response_rate": 75 } ] } ``` --- # Update Phone Number URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2phone_numbersid/ PUT /api/partner/v2/phone\_numbers/{id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/phone_numbers/1'; const options = { method: 'PUT', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"forwarding_number":"+15551234567"}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PUT \ --url https://api.linqapp.com/api/partner/v2/phone_numbers/1 \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "forwarding_number": "+15551234567" }' ``` - Production server api.linqapp.com/api/partner/v2/phone\_numbers/{id} Updates the forwarding number for a phone number. The forwarding number is where calls will be forwarded to when the primary number is unavailable. Pass an empty string to clear the forwarding number. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **id** required integer The phone number ID ## Request Body required Media type application/json object **forwarding\_number** The phone number to forward calls to when unavailable (pass empty string to clear) string nullable ##### Example ``` +15551234567 ``` ## Responses ### 200 Phone number updated successfully Media type application/json object **id** integer **phone\_number** string **forwarding\_number** Phone number where calls are forwarded when this number is unavailable string nullable **response\_rate** Response rate as messages per second integer ##### Example ``` { "id": 99, "phone_number": "+19498151221", "forwarding_number": null, "response_rate": 75 } ``` ### 403 Forbidden - Phone number doesn’t belong to your organization Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 403, "code": "forbidden", "title": "Forbidden", "detail": "This phone number doesn't belong to your organization" } ] } ``` ### 422 Unprocessable entity - Invalid forwarding number Media type application/json Standard error format used by most endpoints (render\_error format) object **errors** Array\<object> object **status** integer **code** string **title** string **detail** string ##### Example ``` { "errors": [ { "status": 422, "code": "invalid_request", "title": "Invalid Request", "detail": "Forwarding number is invalid" } ] } ``` --- # List Webhook Subscriptions URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2webhook_subscriptions/get/ GET /api/partner/v2/webhook\_subscriptions Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/webhook_subscriptions'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://api.linqapp.com/api/partner/v2/webhook_subscriptions \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/webhook\_subscriptions Retrieves all webhook subscriptions for the authenticated partner ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Responses ### 200 Successful response Media type application/json object **webhook\_subscriptions** Array\<object> object **id** integer **webhook\_url** string **version** API version of the webhook subscription integer **events** Array\<string> **active** boolean **secret\_configured** Whether a secret has been configured for this webhook (the actual secret is never returned) boolean **last\_delivered\_at** Timestamp of the last successful webhook delivery string format: date-time nullable **delivery\_attempts** Total number of delivery attempts for this webhook integer **created\_at** string format: date-time **updated\_at** string format: date-time ##### Example ``` { "webhook_subscriptions": [ { "id": 123, "webhook_url": "https://webhook.example.com/linq", "version": 2, "events": [ "message.sent", "message.received", "call.completed" ], "active": true, "secret_configured": true, "last_delivered_at": "2024-01-15T10:30:00Z", "delivery_attempts": 42 } ] } ``` --- # Create Webhook Subscription URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2webhook_subscriptions/post/ POST /api/partner/v2/webhook\_subscriptions Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/webhook_subscriptions'; const options = { method: 'POST', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"webhook_subscription":{"webhook_url":"https://example.com/webhooks/linq","version":2,"secret":"your-webhook-secret-key","active":true,"events":["message.sent","message.received"]}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://api.linqapp.com/api/partner/v2/webhook_subscriptions \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "webhook_subscription": { "webhook_url": "https://example.com/webhooks/linq", "version": 2, "secret": "your-webhook-secret-key", "active": true, "events": [ "message.sent", "message.received" ] } }' ``` - Production server api.linqapp.com/api/partner/v2/webhook\_subscriptions Creates a new webhook subscription for specific event types. **Note:** Creating multiple webhook subscriptions with the same URL and events will result in duplicate webhook deliveries for those events. Ensure you don’t create duplicate subscriptions unless intentional. ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Request Body required Media type application/json object **webhook\_subscription** required object **webhook\_url** required string ##### Example ``` https://example.com/webhooks/linq ``` **version** required API version (should be 2 for v2) integer ##### Example ``` 2 ``` **secret** Secret key for webhook signature verification string ##### Example ``` your-webhook-secret-key ``` **active** Whether the webhook subscription is active boolean ##### Example ``` true ``` **events** required Array\<string> Allowed values: message.sent message.received message.read call.completed reaction.sent reaction.received typing\_indicator.received typing\_indicator.removed chat.created contact.created contact.updated contact.deleted participant.added participant.removed ##### Example ``` [ "message.sent", "message.received" ] ``` ## Responses ### 201 Webhook subscription created successfully Media type application/json object **webhook\_subscription** object **id** integer **webhook\_url** string **version** API version of the webhook subscription integer **events** Array\<string> **active** boolean **secret\_configured** Whether a secret has been configured for this webhook (the actual secret is never returned) boolean **last\_delivered\_at** Timestamp of the last successful webhook delivery string format: date-time nullable **delivery\_attempts** Total number of delivery attempts for this webhook integer **created\_at** string format: date-time **updated\_at** string format: date-time ##### Example ``` { "webhook_subscription": { "id": 123, "webhook_url": "https://webhook.example.com/linq", "version": 2, "events": [ "message.sent", "message.received", "call.completed" ], "active": true, "secret_configured": true, "last_delivered_at": "2024-01-15T10:30:00Z", "delivery_attempts": 42 } } ``` --- # Delete Webhook Subscription URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2webhook_subscriptionswebhook_subscription_id/delete/ DELETE /api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/webhook_subscriptions/1'; const options = { method: 'DELETE', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request DELETE \ --url https://api.linqapp.com/api/partner/v2/webhook_subscriptions/1 \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id} Deletes a webhook subscription ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **webhook\_subscription\_id** required integer The webhook subscription ID ## Responses ### 204 Webhook subscription deleted successfully --- # Update Webhook Subscription URL: https://docs.linqapp.com/v2/api/operations/apipartnerv2webhook_subscriptionswebhook_subscription_id/put/ PUT /api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id} Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/api/partner/v2/webhook_subscriptions/1'; const options = { method: 'PUT', headers: { 'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>', 'Content-Type': 'application/json' }, body: '{"webhook_subscription":{"webhook_url":"https://example.com/webhooks/linq","version":2,"secret":"your-webhook-secret-key","active":true,"events":["message.sent","message.received"]}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PUT \ --url https://api.linqapp.com/api/partner/v2/webhook_subscriptions/1 \ --header 'Content-Type: application/json' \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' \ --data '{ "webhook_subscription": { "webhook_url": "https://example.com/webhooks/linq", "version": 2, "secret": "your-webhook-secret-key", "active": true, "events": [ "message.sent", "message.received" ] } }' ``` - Production server api.linqapp.com/api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id} Updates an existing webhook subscription ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Parameters ### Path Parameters **webhook\_subscription\_id** required integer The webhook subscription ID ## Request Body required Media type application/json object **webhook\_subscription** required object **webhook\_url** required string ##### Example ``` https://example.com/webhooks/linq ``` **version** required API version (should be 2 for v2) integer ##### Example ``` 2 ``` **secret** Secret key for webhook signature verification string ##### Example ``` your-webhook-secret-key ``` **active** Whether the webhook subscription is active boolean ##### Example ``` true ``` **events** required Array\<string> Allowed values: message.sent message.received message.read call.completed reaction.sent reaction.received typing\_indicator.received typing\_indicator.removed chat.created contact.created contact.updated contact.deleted participant.added participant.removed ##### Example ``` [ "message.sent", "message.received" ] ``` ## Responses ### 200 Webhook subscription updated successfully Media type application/json object **webhook\_subscription** object **id** integer **webhook\_url** string **version** API version of the webhook subscription integer **events** Array\<string> **active** boolean **secret\_configured** Whether a secret has been configured for this webhook (the actual secret is never returned) boolean **last\_delivered\_at** Timestamp of the last successful webhook delivery string format: date-time nullable **delivery\_attempts** Total number of delivery attempts for this webhook integer **created\_at** string format: date-time **updated\_at** string format: date-time ##### Example ``` { "webhook_subscription": { "id": 123, "webhook_url": "https://webhook.example.com/linq", "version": 2, "events": [ "message.sent", "message.received", "call.completed" ], "active": true, "secret_configured": true, "last_delivered_at": "2024-01-15T10:30:00Z", "delivery_attempts": 42 } } ``` --- # Overview URL: https://docs.linqapp.com/v2/api/operations/tags/chat-messages/ ## Chat Messages Chat Messages are individual messages within a Chat thread. Messages can include text, attachments, and reactions. All messages are associated with a specific Chat and sent from a phone number in your organization. ## Operations GET [/api/partner/v2/chats/{chat\_id}/chat\_messages](/v2/api/operations/apipartnerv2chatschat_idchat_messages/get/index.md) POST [/api/partner/v2/chats/{chat\_id}/chat\_messages](/v2/api/operations/apipartnerv2chatschat_idchat_messages/post/index.md) GET [/api/partner/v2/chats/{chat\_id}/chat\_messages/{id}](/v2/api/operations/apipartnerv2chatschat_idchat_messagesid/get/index.md) DELETE [/api/partner/v2/chats/{chat\_id}/chat\_messages/{id}](/v2/api/operations/apipartnerv2chatschat_idchat_messagesid/delete/index.md) POST [/api/partner/v2/chats/{chat\_id}/chat\_messages/{id}/edit](/v2/api/operations/apipartnerv2chatschat_idchat_messagesidedit/index.md) POST [/api/partner/v2/chat\_messages/{chat\_message\_id}/reactions](/v2/api/operations/apipartnerv2chat_messageschat_message_idreactions/index.md) GET [/api/partner/v2/chat\_message\_reactions/{reaction\_id}](/v2/api/operations/apipartnerv2chat_message_reactionsreaction_id/index.md) --- # Overview URL: https://docs.linqapp.com/v2/api/operations/tags/chats/ ## Chats A Chat is a collection of Chat Messages. To begin a chat thread, you must create a Chat with at least one phone\_number and one message. Including multiple phone\_numbers would create a group Chat. The phone\_number that all messages will originate from is based on your X-LINQ-INTEGRATION-TOKEN. You do not have to include your phone\_number in the phone\_numbers array when creating a Chat, only the recipients’ phone\_numbers. A PhoneNumber is always normalized on Linq’s end to assume a US country code, and append a +1 where no country code is provided. Phone Number format should be +12223334444 or 2223334444. ## Operations GET [/api/partner/v2/chats](/v2/api/operations/apipartnerv2chats/get/index.md) POST [/api/partner/v2/chats](/v2/api/operations/apipartnerv2chats/post/index.md) GET [/api/partner/v2/chats/{chat\_id}](/v2/api/operations/apipartnerv2chatschat_id/index.md) GET [/api/partner/v2/chats/find](/v2/api/operations/apipartnerv2chatsfind/index.md) PUT [/api/partner/v2/chats/{chat\_id}/mark\_as\_read](/v2/api/operations/apipartnerv2chatschat_idmark_as_read/index.md) POST [/api/partner/v2/chats/{chat\_id}/start\_typing](/v2/api/operations/apipartnerv2chatschat_idstart_typing/index.md) DELETE [/api/partner/v2/chats/{chat\_id}/stop\_typing](/v2/api/operations/apipartnerv2chatschat_idstop_typing/index.md) POST [/api/partner/v2/chats/{chat\_id}/share\_contact](/v2/api/operations/apipartnerv2chatschat_idshare_contact/index.md) --- # Overview URL: https://docs.linqapp.com/v2/api/operations/tags/contacts/ ## Contacts Contacts represent people in your organization’s address book. Each contact can be assigned to a specific user in your organization. If no user is specified when creating a contact, it will be automatically assigned to the first admin user. ## Operations GET [/api/partner/v2/contacts](/v2/api/operations/apipartnerv2contacts/get/index.md) POST [/api/partner/v2/contacts](/v2/api/operations/apipartnerv2contacts/post/index.md) GET [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/get/index.md) PUT [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/put/index.md) DELETE [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/delete/index.md) PATCH [/api/partner/v2/contacts/{id}](/v2/api/operations/apipartnerv2contactsid/patch/index.md) GET [/api/partner/v2/contacts/find](/v2/api/operations/apipartnerv2contactsfind/index.md) --- # Overview URL: https://docs.linqapp.com/v2/api/operations/tags/phone-numbers/ ## Phone Numbers Phone Numbers represent the messaging-enabled phone numbers in your organization, supporting iMessage, RCS, and SMS. Each phone number is associated with a user and can be used to send and receive messages. Messages are automatically sent using the best available protocol (iMessage → RCS → SMS). Phone numbers can have forwarding numbers configured. ## Operations GET [/api/partner/v2/phone\_numbers](/v2/api/operations/apipartnerv2phone_numbers/index.md) PUT [/api/partner/v2/phone\_numbers/{id}](/v2/api/operations/apipartnerv2phone_numbersid/index.md) --- # Overview URL: https://docs.linqapp.com/v2/api/operations/tags/utilities/ ## Utilities Utility endpoints for checking iMessage availability and other helper functions. Note that messages automatically use the best available protocol (iMessage → RCS → SMS). ## Operations POST [/api/partner/v2/i\_message\_availability/check](/v2/api/operations/apipartnerv2i_message_availabilitycheck/index.md) --- # Overview URL: https://docs.linqapp.com/v2/api/operations/tags/webhook-events-documentation/ ## Webhook Events Documentation Documentation for all available webhook events and their payload structures. ## Operations GET [/webhooks/events](/v2/api/operations/webhooksevents/index.md) --- # Overview URL: https://docs.linqapp.com/v2/api/operations/tags/webhook-subscriptions/ ## Webhook Subscriptions Webhook Subscriptions allow you to receive real-time notifications when events occur in your organization. Configure webhook endpoints to receive events such as messages sent/received, reactions, typing indicators, and more. ## Operations GET [/api/partner/v2/webhook\_subscriptions](/v2/api/operations/apipartnerv2webhook_subscriptions/get/index.md) POST [/api/partner/v2/webhook\_subscriptions](/v2/api/operations/apipartnerv2webhook_subscriptions/post/index.md) PUT [/api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id}](/v2/api/operations/apipartnerv2webhook_subscriptionswebhook_subscription_id/put/index.md) DELETE [/api/partner/v2/webhook\_subscriptions/{webhook\_subscription\_id}](/v2/api/operations/apipartnerv2webhook_subscriptionswebhook_subscription_id/delete/index.md) --- # Webhook Events Reference (Documentation Only) URL: https://docs.linqapp.com/v2/api/operations/webhooksevents/ GET /webhooks/events Select code sample cURL (shell:curl) ``` const url = 'https://api.linqapp.com/webhooks/events'; const options = { method: 'GET', headers: {'X-LINQ-INTEGRATION-TOKEN': '<X-LINQ-INTEGRATION-TOKEN>'} }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://api.linqapp.com/webhooks/events \ --header 'X-LINQ-INTEGRATION-TOKEN: <X-LINQ-INTEGRATION-TOKEN>' ``` - Production server api.linqapp.com/webhooks/events **Note: This is a documentation endpoint only - it does not exist in the actual API** ## Webhook Events Overview Linq V2 provides real-time event notifications via webhooks. When specific events occur in your Linq account, we send HTTP POST requests to your configured endpoint. ## Available Webhook Events | Event Type | Description | When Triggered | | --------------------------- | ------------------------------------ | ----------------------------------------------------------- | | `message.sent` | Message sent from Linq | When a message is sent from a Linq user | | `message.received` | Message received by Linq | When a message is received by a Linq user | | `message.read` | Message marked as read | When messages are marked as read | | `call.completed` | Call completed | When a voice call has been completed | | `reaction.sent` | Reaction sent by a Linq number | When a Linq number sends a reaction to a message | | `reaction.received` | Reaction received from another party | When a reaction is received from another party | | `typing_indicator.received` | Typing indicator displayed | When a typing indicator is displayed | | `typing_indicator.removed` | Typing indicator removed | When a typing indicator is removed | | `chat.created` | Chat created | When a new chat is created via the API | | `contact.created` | Contact created | When a contact is created via the API | | `contact.updated` | Contact updated | When a contact is updated via the API | | `contact.deleted` | Contact deleted | When a contact is deleted via the API | | `participant.added` | Participant added to group chat | When someone is added to a group conversation | | `participant.removed` | Participant removed from group chat | When someone leaves or is removed from a group conversation | ## Webhook Payload Structure All webhook events follow this standard format: ``` { "api_version": "v2", "created_at": "2025-06-01T10:00:00-06:00", "data": { // Event-specific data }, "event_id": "9dabceb9-9194-4dc6-beda-892573f377b4", "event_type": "message.sent" } ``` **Field Descriptions:** - `event_type`: The type of webhook event (e.g., “message.sent”, “message.received”) - `api_version`: Always “v2” for V2 webhooks - `data`: Event-specific payload data (structure varies by event type) - `created_at`: Timestamp when the event was created (ISO 8601 format with timezone) - `event_id`: Unique identifier for this webhook event (UUID format) ## Webhook Signature Verification Linq signs all webhook requests with a secret key to allow you to verify that webhooks are genuinely from Linq. When creating a webhook subscription, you can optionally provide a `secret` value. If provided, Linq will include an `X-Webhook-Signature` header with each webhook request. ### Signature Format The signature is computed using HMAC-SHA256: ``` X-Webhook-Signature: sha256=<hex_digest> ``` ### Verifying Signatures To verify a webhook signature: 1. **Extract the signature** from the `X-Webhook-Signature` header 2. **Compute the expected signature** using HMAC-SHA256 with your webhook secret and the raw request body 3. **Compare** the computed signature with the received signature **Important:** The signature is computed on the canonical JSON format (with keys sorted recursively). Make sure to use the raw request body exactly as received. ### Example Verification (Ruby) ``` require 'openssl' require 'json' def verify_webhook_signature(secret, payload, signature_header) # Extract the hex digest from the header expected_signature = signature_header.sub(/^sha256=/, '') # Compute HMAC-SHA256 of the raw payload computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, payload) # Compare signatures securely ActiveSupport::SecurityUtils.secure_compare(computed_signature, expected_signature) end # Usage in your webhook endpoint secret = 'your_webhook_secret' payload = request.raw_post signature = request.headers['X-Webhook-Signature'] if verify_webhook_signature(secret, payload, signature) # Signature is valid, process webhook else # Invalid signature, reject request render json: { error: 'Invalid signature' }, status: :unauthorized end ``` ### Example Verification (Node.js) ``` const crypto = require('crypto'); function verifyWebhookSignature(secret, payload, signatureHeader) { // Extract the hex digest from the header const expectedSignature = signatureHeader.replace(/^sha256=/, ''); // Compute HMAC-SHA256 of the raw payload const computedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); // Compare signatures securely (constant-time comparison) return crypto.timingSafeEqual( Buffer.from(computedSignature), Buffer.from(expectedSignature) ); } // Usage in your webhook endpoint const secret = 'your_webhook_secret'; const payload = req.rawBody; // Raw request body as string const signature = req.headers['x-webhook-signature']; if (verifyWebhookSignature(secret, payload, signature)) { // Signature is valid, process webhook } else { // Invalid signature, reject request res.status(401).json({ error: 'Invalid signature' }); } ``` ## Event Payload Examples ### Message Sent/Received Event ``` { "api_version": "v2", "created_at": "2025-05-21T15:30:00-06:00", "data": { "id": "27799380", "chat_id": "324324", "text": "Hello, how are you?", "sent_at": "2025-05-21 15:30:00 -0600", "is_read": false, "from_phone": "+15551234567", "service": "iMessage", "reaction_id": null, "chat_handles": [ { "identifier": "+15551234567", "display_name": "John Doe", "is_me": false }, { "identifier": "+15559876543", "display_name": "Your Linq Number", "is_me": true } ], "attachments": [ { "id": "abc12345-1234-5678-9abc-def012345678", "url": "https://storage.googleapis.com/linq-files/attachments/abc123.pdf", "filename": "document.pdf", "mime_type": "application/pdf" } ] }, "event_id": "9dabceb9-9194-4dc6-beda-892573f377b4", "event_type": "message.received" } ``` ### Message Read Event ``` { "api_version": "v2", "created_at": "2025-05-21T15:35:00-06:00", "data": { "id": "12345", "chat_id": "67890", "read_at": "2025-05-21T15:35:00-06:00" }, "event_id": "wh_evt_789ghi", "event_type": "message.read" } ``` ### Call Completed Event ``` { "api_version": "v2", "created_at": "2025-05-21T15:05:30-06:00", "data": { "id": "12345", "to": "+15559876543", "from": "+15551234567", "status": "completed", "direction": "inbound", "start_time": "2025-05-21 15:00:00 -0600", "end_time": "2025-05-21 15:05:30 -0600", "duration": 330, "call_type": "normal", "forwarded_from": null, "answered_by": "+15559876543", "voicemail": false, "has_recording": true, "recording_data": { "id": "67890", "url": "https://storage.googleapis.com/linq-files/recordings/abc123.mp3", "duration": 330, "transcribed": true, "transcript_available": true, "transcription": "Hello, this is John calling about our meeting tomorrow...", "summary": "John called to confirm tomorrow's meeting at 2pm." } }, "event_id": "wh_evt_101jkl", "event_type": "call.completed" } ``` ### Contact Created/Updated Event ``` { "api_version": "v2", "created_at": "2025-07-30T10:00:00-06:00", "data": { "id": 123, "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "email": "john@example.com", "phone_number": "+15551234567", "company": "Acme Corp", "title": "CEO", "location": "San Francisco, CA", "image_url": null, "created_at": "2025-07-30T10:00:00-06:00", "updated_at": "2025-07-30T10:00:00-06:00", "contact_owner": { "id": 456, "email": "owner@company.com", "first_name": "Jane", "last_name": "Owner", "name": "Jane Owner" } }, "event_id": "wh_evt_contact_123", "event_type": "contact.created" } ``` ### Contact Deleted Event ``` { "api_version": "v2", "created_at": "2025-07-30T10:00:00-06:00", "data": { "id": 123 }, "event_id": "wh_evt_contact_deleted_123", "event_type": "contact.deleted" } ``` **Note:** The `contact.deleted` event only includes the contact ID, not the full contact data or owner information. ### Chat Created Event ``` { "api_version": "v2", "created_at": "2025-05-21T15:30:00-06:00", "data": { "chat_id": "67890", "service": "iMessage", "is_direct": true, "is_group": false, "display_name": "John Doe", "chat_handles": [ { "id": "123", "identifier": "+15551234567", "display_name": "John Doe", "is_me": false, "is_departed": false }, { "id": "124", "identifier": "+15559876543", "display_name": "Your Linq Number", "is_me": true, "is_departed": false } ], "created_at": "2025-05-21T15:30:00-06:00" }, "event_id": "wh_evt_chat_123", "event_type": "chat.created" } ``` ### Reaction Sent/Received Event The `reaction` field supports the following values: - `love` - Love/heart reaction - `like` - Like/thumbs up reaction - `dislike` - Dislike/thumbs down reaction - `laugh` - Laugh/haha reaction - `emphasize` - Emphasize/!! reaction - `question` - Question/? reaction ``` { "api_version": "v2", "created_at": "2025-07-10T15:31:00-06:00", "data": { "id": "12346", "chat_message_id": "67891", "chat_id": "11111", "reaction": "like", "is_from_me": false, "sent_at": "2025-07-10 15:31:00 -0600", "created_at": "2025-07-10 15:31:00 -0600", "chat_handle": { "id": "22223", "identifier": "+15551234567" }, "chat_handles": [ { "identifier": "+15551234567", "display_name": "John Doe", "is_me": false }, { "identifier": "+15559876543", "display_name": "Your Linq Number", "is_me": true } ], "associated_message": { "id": "67891", "text": "I'm doing great, thanks!", "sent_at": "2025-07-10 15:30:30 -0600" } }, "event_id": "wh_evt_456deg", "event_type": "reaction.received" } ``` ### Typing Indicator Received/Removed Event ``` { "api_version": "v2", "created_at": "2025-07-10T15:32:00-06:00", "data": { "chat_id": "11111", "display": true, "timestamp": "2025-07-10 15:32:00 -0600", "chat_handles": [ { "identifier": "+15551234567", "display_name": "John Doe", "is_me": false }, { "identifier": "+15559876543", "display_name": "Your Linq Number", "is_me": true } ] }, "event_id": "wh_evt_789ghi", "event_type": "typing_indicator.received" } ``` ## Webhook Security ### Signature Verification All webhooks include a signature in the `X-Webhook-Signature` header. Verify the signature to ensure the webhook is from Linq: ``` const crypto = require('crypto'); function verifyWebhookSignature(body, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); return `sha256=${expectedSignature}` === signature; } ``` ### Python Example ``` import hmac import hashlib def verify_webhook(payload, signature, secret): expected = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() return f"sha256={expected}" == signature ``` ## Integration Steps 1. **Create a webhook endpoint** on your server that can receive POST requests 2. **Create a webhook subscription** using `POST /api/partner/v2/webhook_subscriptions` 3. **Store your webhook secret** securely for signature verification 4. **Verify signatures** on all incoming webhooks 5. **Respond with HTTP 200** within 10 seconds to acknowledge receipt 6. **Process webhooks asynchronously** to avoid timeouts ## Testing Webhooks Use tools like ngrok or webhook.site to test webhook integrations during development: ``` # Using ngrok ngrok http 3000 # Then use the ngrok URL as your webhook endpoint ``` ## Error Handling Your webhook endpoint should: - Return HTTP 200 for successful receipt - Return appropriate error codes (400, 500) for failures - Log all webhook events for debugging - Implement idempotency to handle duplicate events ## Authorizations - **[ApiKeyAuth](/v2/api/#apikeyauth/index.md)** ## Responses ### 200 This endpoint is for documentation purposes only --- # Authentication URL: https://docs.linqapp.com/v2/authentication/ The Linq Partner API uses **integration tokens** for authentication. Your integration token determines which phone numbers you can message from and which organization data you can access. Integration tokens are provided by your Linq representative. Contact your account manager to request a token or manage existing tokens. > **Your integration token is a secret.** Keep it to yourself and don’t push it to client-side code. Use environment variables or a secret-key system to securely load your integration token into your project. Integration tokens should be provided via the `X-LINQ-INTEGRATION-TOKEN` header. ``` X-LINQ-INTEGRATION-TOKEN: your_token_here ``` ## Making authenticated requests Include your token in the header of every API request: Terminal window ``` curl "https://api.linqapp.com/api/partner/v2/chats?phone_number=%2B19998887777" \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" ``` ## Base URL All API requests use the following base URL: ``` https://api.linqapp.com ``` Combine this base URL with the endpoint paths shown in the [V2 Reference](/v2/api/index.md). For example: - List chats: `https://api.linqapp.com/api/partner/v2/chats?phone_number=...` - Send a message: `https://api.linqapp.com/api/partner/v2/chats/{chat_id}/chat_messages` ## Next step Send your first message in [Your First Message](/v2/your-first-message/index.md). --- # Introduction URL: https://docs.linqapp.com/v2/ V2 is legacy These docs are for our legacy V2 API. If you’re starting a new integration, use the [V3 API](/index.md) instead — see the [Quickstart](/getting-started/quickstart/index.md) to get going in minutes. ## Welcome to the Linq Partner API The Linq Partner API enables you to integrate iMessage, RCS, and SMS messaging directly into your applications. Build powerful conversational experiences, automate customer communication, and leverage the same messaging infrastructure trusted by 50,000+ teams. ### What you can build With the Partner API, you can: - **Send and receive iMessage, RCS, and SMS messages** at scale through your organization’s phone numbers - **Start and manage group chats** between your app and multiple users - **Automate conversations** with customers, leads, and team members - **Receive real-time notifications** via webhooks for incoming messages, reactions, and events - **Manage contacts** across your organization - **Track message delivery** with read receipts and delivery status ### Who is this for? The Partner API is designed for: - **Software platforms** building messaging into their product - **CRM and sales tools** adding conversational AI capabilities - **Customer support platforms** offering multi-channel communication - **Marketing automation tools** engaging customers via text - **Developers** building custom messaging solutions ### Key features #### Unified messaging Send iMessages and RCS with rich media (images, videos, documents) and automatically fall back to SMS when needed. One API for all three channels. #### Real-time webhooks Receive instant notifications when messages arrive, contacts are updated, or users interact with your messages through reactions and typing indicators. #### Full message features Access the complete iMessage and RCS feature set: - Read receipts and typing indicators - Message reactions (love, like, laugh, etc.) - Message editing and deletion - Attachments and rich media - Group conversations #### Secure and reliable All messages are sent through real phone numbers in your organization. Your integration token controls access and determines which phone numbers you can message from. #### Contact management Maintain a centralized address book with automatic user assignment and organization-wide visibility. ### Use cases #### Customer support automation Build AI-powered support agents that handle customer inquiries via text, with seamless handoff to human agents when needed. #### Sales engagement Automate personalized outreach campaigns and follow-ups. Track responses and engagement in real-time. #### Appointment reminders Send automated reminders for appointments, meetings, or events with two-way confirmation. #### Lead qualification Qualify inbound leads through conversational flows before routing to your sales team. #### Order notifications Keep customers informed about order status, shipping updates, and delivery notifications. ### Getting started Ready to start building? Start with [Authentication](/v2/authentication/index.md) and then send your first message in under 5 minutes via [Your First Message](/v2/your-first-message/index.md). Browse the full endpoint catalog under [V2 Reference](/v2/api/index.md). Need help? Contact your Linq representative for API token provisioning and technical support. --- # Your First Message URL: https://docs.linqapp.com/v2/your-first-message/ Get up and running with the Linq Partner API in under 5 minutes. This guide walks you through sending your first message and receiving webhook notifications. ## Prerequisites Before you begin, ensure you have: - A Linq Partner API integration token - At least one phone number provisioned in your organization - Basic familiarity with REST APIs and webhooks **Don’t have an API token yet?** Contact your Linq representative to get started. ## Step 1: Send your first message Let’s send a simple text message to create a new chat conversation. ### Create a chat and send message Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v2/chats \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" \ -H "Content-Type: application/json" \ -d '{ "send_from": "+19998887777", "chat": { "phone_numbers": ["+15551234567"] }, "message": { "text": "Hello! This is my first message via the Linq API." } }' ``` **Parameters:** - `send_from` — Your phone number to send from (must be in your organization) - `chat.phone_numbers` — Array of recipient phone numbers (include country code) - `message.text` — The text message to send ### Response ``` { "data": { "id": 12345, "display_name": "+1 (555) 123-4567", "service": "iMessage", "group": false, "chat_handles": [ { "id": 123, "phone_number": "+15551234567", "service": "iMessage", "joined_at": "2025-10-22T10:30:00.000-05:00" } ], "chat_messages": { "id": 67890, "text": "Hello! This is my first message via the Linq API.", "sent_at": "2025-10-22T10:30:00.000-05:00", "delivered_at": null, "delivery_status": "pending", "is_read": false, "attachments": [] } } } ``` You’ve sent your first message. The response includes both the chat and message details. ## Step 2: List your phone numbers Check which phone numbers are available in your organization: Terminal window ``` curl https://api.linqapp.com/api/partner/v2/phone_numbers \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" ``` ### Response ``` { "phone_numbers": [ { "id": 99, "phone_number": "+19998887777", "forwarding_number": null, "response_rate": 75 } ] } ``` ## Step 3: Receive messages with webhooks To receive incoming messages, set up a webhook subscription. ### Create webhook subscription Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v2/webhook_subscriptions \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" \ -H "Content-Type: application/json" \ -d '{ "webhook_subscription": { "webhook_url": "https://your-app.com/webhooks/linq", "events": ["message.received", "message.sent"], "version": 2, "active": true } }' ``` ### Webhook payload example When a message is received, Linq will POST to your webhook URL: ``` { "api_version": "v2", "created_at": "2025-10-22T10:31:00-06:00", "data": { "id": "67891", "chat_id": "12345", "text": "Thanks! This is my reply.", "sent_at": "2025-10-22 10:31:00 -0600", "is_read": false, "from_phone": "+15551234567", "service": "iMessage", "reaction_id": null, "chat_handles": [ { "identifier": "+15551234567", "display_name": "John Doe", "is_me": false }, { "identifier": "+19998887777", "display_name": "Your Linq Number", "is_me": true } ], "attachments": [] }, "event_id": "9dabceb9-9194-4dc6-beda-892573f377b4", "event_type": "message.received" } ``` > **Important:** Always validate webhook signatures and implement idempotency to handle duplicate events. ## Step 4: Send a message with attachments Enhance your messages with images, videos, or documents: Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v2/chats/12345/chat_messages \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" \ -F "message[text]=Check out this image!" \ -F "message[attachment_urls][]=https://your-cdn.com/image.jpg" ``` To upload files directly instead of referencing URLs, use `message[attachments][]` with `@/path/to/file`: Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v2/chats/12345/chat_messages \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" \ -F "message[text]=Check out these files" \ -F "message[attachments][]=@/path/to/image.jpg" \ -F "message[attachments][]=@/path/to/document.pdf" ``` **Supported file types:** Images (JPG, PNG, GIF), videos (MP4, MOV), documents (PDF, DOCX), and more. ## Step 5: React to messages Add reactions to messages for engagement tracking: Terminal window ``` curl -X POST https://api.linqapp.com/api/partner/v2/chat_messages/67891/reactions \ -H "X-LINQ-INTEGRATION-TOKEN: your_token_here" \ -H "Content-Type: application/json" \ -d '{ "type": "love", "operation": "add" }' ``` **Available reactions:** `love`, `like`, `dislike`, `laugh`, `emphasize`, `question` ## Phone number formatting All phone numbers must follow these rules: - **Format:** `+12223334444` or `2223334444` - **Country code:** US country code (`+1`) is assumed if not provided - **Normalization:** Linq automatically normalizes numbers on the backend Valid: `+15551234567`, `5551234567`. Invalid: `555-123-4567`, `(555) 123-4567`. ## Rate limits API requests are rate-limited to ensure system stability. If you exceed your limit, you’ll receive a `429 Too Many Requests` response. Contact your Linq representative to discuss your rate limits or request an increase based on your use case. ## Next steps Now that you’ve sent your first messages, explore the full API capabilities: - [**Webhook Events**](/v2/api/operations/tags/webhook-events-documentation/index.md) — All available event types - [**Contact Management**](/v2/api/operations/tags/contacts/index.md) — Manage your address book - [**Chat Operations**](/v2/api/operations/tags/chats/index.md) — Group chats, read receipts, typing indicators ## Need help? - **Technical issues** — Contact your Linq representative - **Feature requests** — Share feedback with your account team - **Integration questions** — Consult the [V2 Reference](/v2/api/index.md) for detailed examples Ready to build? Dive into the [V2 Reference](/v2/api/index.md) to explore all available endpoints and features.