Skip to content
Linq

API Reference

API Overview

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: [email protected]
  • No spaces, dashes, or parentheses in phone numbers
Create a new chat
POST/v3/chats
List all chats
GET/v3/chats
Get a chat by ID
GET/v3/chats/{chatId}
Mark chat as read
POST/v3/chats/{chatId}/read
Leave a group chat
POST/v3/chats/{chatId}/leave

ChatsParticipants

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: [email protected]
  • No spaces, dashes, or parentheses in phone numbers
Add a participant to a chat
POST/v3/chats/{chatId}/participants
Remove a participant from a chat
DELETE/v3/chats/{chatId}/participants

ChatsTyping

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: [email protected]
  • No spaces, dashes, or parentheses in phone numbers
Start typing indicator
POST/v3/chats/{chatId}/typing
Stop typing indicator
DELETE/v3/chats/{chatId}/typing

ChatsMessages

Messages are individual communications within a chat thread.

Messages can include text, media attachments, rich link previews, and reactions. All messages are associated with a specific chat and sent from a phone number you own.

Messages support delivery status tracking and read receipts.

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 URL inside a text part renders a preview too.

Limitations:

  • A message carries exactly one part, so a link is never combined with text or media.
  • Maximum URL length: 2,048 characters.
Send a message to an existing chat
POST/v3/chats/{chatId}/messages
Get messages from a chat
GET/v3/chats/{chatId}/messages

Messages

Messages are individual communications within a chat thread.

Messages can include text, media attachments, rich link previews, and reactions. All messages are associated with a specific chat and sent from a phone number you own.

Messages support delivery status tracking and read receipts.

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 URL inside a text part renders a preview too.

Limitations:

  • A message carries exactly one part, so a link is never combined with text or media.
  • Maximum URL length: 2,048 characters.
Get all messages in a thread
GET/v3/messages/{messageId}/thread
Get a message by ID
GET/v3/messages/{messageId}
Delete a message from system
DELETE/v3/messages/{messageId}
Add or remove a reaction to a message
POST/v3/messages/{messageId}/reactions

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 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 reusable 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 (stays valid unless deleted — see Attachment Lifetime)

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.

Attachment Lifetime

An attachment_id and its CDN URL stay valid until you delete the file with DELETE /v3/attachments/{attachmentId}.

Deletion is not reversible, and there is no attachment.deleted webhook.

Domain Allowlisting

Attachment URLs in API responses are served from cdn.linqapp.com. This includes:

  • url fields in media 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

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, 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 this layout:

https://cdn.linqapp.com/attachments/partners/{partner_id}/{attachment_id}/{filename}

The URL itself does not expire, but see Attachment Lifetime.

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.

Deleting an Attachment

To permanently remove an attachment you own, use:

DELETE /v3/attachments/{attachmentId}
Authorization: Bearer <your_api_key>

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:

StatusMeaning
204 No ContentDeletion succeeded. The attachment is removed from Linq storage.
400 Bad RequestattachmentId is not a valid UUID.
401 UnauthorizedMissing or invalid API key.
404 Not FoundAttachment does not exist or is not owned by your account.
500 Internal Server ErrorTransient 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.

You can acknowledge the webhook without fetching the file inline, and lazy-load via GET /v3/attachments/{attachmentId} later.

Data Lifecycle Summary

DataRetention
Attachment bytesRetained until you DELETE
Attachment metadata (id, filename, mime type, size)Retained until you DELETE
Message body & partsRetained per message-retention policy
Audit log of deletionsRetained 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.
  • Implement DELETE /v3/attachments/{attachmentId} calls in your deletion workflow.
  • 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.
Pre-upload a file
POST/v3/attachments
Get attachment metadata
GET/v3/attachments/{attachmentId}
Delete an attachment
DELETE/v3/attachments/{attachmentId}

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 or listing chats, use one of your assigned phone numbers in the from field.

Ineligible numbers. A number can temporarily lose the ability to deliver messages. While it is in that state, requests that would produce new activity on it — sending a message, creating a chat, reacting, typing, group actions — are rejected with 403 (error code 2027) before anything is created. Reads keep working, so your existing chats, messages, and history stay available. Omit from on POST /v3/messages and we pick an eligible number for you, skipping ineligible ones; if none of your assigned numbers are eligible, you get 409 (no from number was ever chosen, so there’s no specific number to blame with a 403).

List phone numbers
GET/v3/phone_numbers

Blocked Handles

Block handles — phone numbers, email addresses, SMS short codes, or sender IDs. Inbound messages from a blocked handle are dropped before they reach your webhooks, and direct sends to a blocked handle are rejected with 403 (error code 2026). Group sends that include unblocked members are not restricted.

List blocked handles
GET/v3/blocked_handles
Block a handle
POST/v3/blocked_handles
Unblock a handle
DELETE/v3/blocked_handles

Webhook Events

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 with exponential backoff for up to 30 minutes. 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 specification. You can safely ignore them if your current verification code works and you don’t want to use this convention.

Used by our SDK and any Standard Webhooks library.

HeaderDescription
webhook-idUnique event identifier (use as idempotency key)
webhook-timestampUnix timestamp (seconds) when the webhook was sent
webhook-signatureStandard 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.

HeaderDescription
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. You can use any Standard Webhooks library 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

Webhook Subscriptions

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 with exponential backoff for up to 30 minutes. 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 specification. You can safely ignore them if your current verification code works and you don’t want to use this convention.

Used by our SDK and any Standard Webhooks library.

HeaderDescription
webhook-idUnique event identifier (use as idempotency key)
webhook-timestampUnix timestamp (seconds) when the webhook was sent
webhook-signatureStandard 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.

HeaderDescription
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. You can use any Standard Webhooks library 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
Create a new webhook subscription
POST/v3/webhook-subscriptions
List all webhook subscriptions
GET/v3/webhook-subscriptions
Get a webhook subscription by ID
GET/v3/webhook-subscriptions/{subscriptionId}
Update a webhook subscription
PUT/v3/webhook-subscriptions/{subscriptionId}
Delete a webhook subscription
DELETE/v3/webhook-subscriptions/{subscriptionId}

Webhooks