Skip to main content

Overview

Messages are individual DMs sent or received within threads. This guide covers sending messages, retrieving message history, and handling common messaging scenarios.

Sending Messages

There are two ways to send messages: within an existing thread, or using quick send.

Standard Send (Existing Thread)

Send a message in a thread you’ve already created or looked up:
const { data } = await client.post(`/threads/${threadId}/messages`, {
  text: 'Thanks for your interest! How can I help you today?'
});

console.log('Message sent:', data.message.id);
Response:
{
  "message": {
    "id": "msg_abc123",
    "threadId": "thread_xyz789",
    "text": "Thanks for your interest! How can I help you today?",
    "direction": "outbound",
    "createdAt": "2025-11-23T16:45:00.000Z",
    "platformId": "1234567890"
  }
}

Quick Send

Quick send combines thread lookup/creation and message sending in one request:
const { data } = await client.post('/threads/messages', {
  prospectPlatformId: '987654321',
  accountLinkId: 'acc_abc123',
  text: 'Hi! I saw your tweet and wanted to reach out.'
});

console.log('Quick send successful:', data.message.id);
console.log('Thread ID:', data.message.threadId);
When to use quick send:
  • One-off messages
  • You don’t need the thread object
  • Simplifying your code flow
When to use standard send:
  • You need thread details
  • Sending multiple messages in sequence
  • You’re already working with a thread object

Retrieving Messages

Get message history for a thread:
const { data } = await client.get(`/threads/${threadId}/messages`, {
  params: {
    limit: 50
  }
});

console.log(`Found ${data.messages.length} messages`);

data.messages.forEach(message => {
  const prefix = message.direction === 'inbound' ? '←' : '→';
  console.log(`${prefix} ${message.text}`);
});
Response structure:
{
  "messages": [
    {
      "id": "msg_001",
      "threadId": "thread_abc",
      "text": "Hi, I'm interested in your product",
      "direction": "inbound",
      "createdAt": "2025-11-23T10:00:00.000Z"
    },
    {
      "id": "msg_002",
      "threadId": "thread_abc",
      "text": "Great! Let me tell you more about it",
      "direction": "outbound",
      "createdAt": "2025-11-23T10:05:00.000Z"
    }
  ],
  "nextCursor": {
    "id": "msg_002",
    "timestamp": "2025-11-23T10:05:00.000Z"
  }
}

Message Pagination

For long conversations, paginate through messages:
async function getAllMessages(threadId: string) {
  const allMessages = [];
  let cursor = undefined;

  do {
    const { data } = await client.get(`/threads/${threadId}/messages`, {
      params: {
        cursor,
        limit: 100
      }
    });

    allMessages.push(...data.messages);
    cursor = data.nextCursor;

  } while (cursor);

  return allMessages;
}
Messages are returned in reverse chronological order (newest first) by default.

Message Properties

Direction

  • inbound: Messages received from the prospect
  • outbound: Messages sent by your team
// Filter to only outbound messages
const outboundMessages = messages.filter(m => m.direction === 'outbound');

// Check who sent last message
const lastMessage = messages[0];
const waitingForReply = lastMessage.direction === 'outbound';

Timestamps

// Sort messages chronologically (oldest first)
const chronological = messages.sort((a, b) =>
  new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);

// Find messages from last 24 hours
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recent = messages.filter(m =>
  new Date(m.createdAt) > oneDayAgo
);

Common Workflows

Send a Follow-Up Message

async function sendFollowUp(threadId: string) {
  // Check if we sent last message
  const { data } = await client.get(`/threads/${threadId}/messages`, {
    params: { limit: 1 }
  });

  const lastMessage = data.messages[0];

  if (lastMessage.direction === 'outbound') {
    // We're still waiting for reply
    await client.post(`/threads/${threadId}/messages`, {
      text: 'Just following up on my previous message. Let me know if you have any questions!'
    });

    console.log('Follow-up sent');
  }
}

Auto-Respond to New Messages

async function autoRespond(threadId: string, trigger: string, response: string) {
  const { data } = await client.get(`/threads/${threadId}/messages`, {
    params: { limit: 1 }
  });

  const lastMessage = data.messages[0];

  if (
    lastMessage.direction === 'inbound' &&
    lastMessage.text.toLowerCase().includes(trigger)
  ) {
    await client.post(`/threads/${threadId}/messages`, {
      text: response
    });

    console.log('Auto-response sent');
  }
}

// Usage
await autoRespond(
  threadId,
  'pricing',
  'Our pricing starts at $99/month. Would you like to schedule a demo?'
);

Calculate Response Time

async function calculateResponseTime(threadId: string) {
  const { data } = await client.get(`/threads/${threadId}/messages`);

  const responseTimes = [];

  for (let i = 0; i < data.messages.length - 1; i++) {
    const current = data.messages[i];
    const previous = data.messages[i + 1];

    // Check if this is an outbound response to inbound message
    if (current.direction === 'outbound' && previous.direction === 'inbound') {
      const responseTime =
        new Date(current.createdAt).getTime() -
        new Date(previous.createdAt).getTime();

      responseTimes.push(responseTime);
    }
  }

  const avgResponseTime =
    responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length;

  const avgMinutes = Math.round(avgResponseTime / 1000 / 60);
  console.log(`Average response time: ${avgMinutes} minutes`);

  return avgMinutes;
}

Bulk Send Messages

Send messages to multiple threads:
async function bulkSendMessages(threadIds: string[], messageText: string) {
  const results = [];

  for (const threadId of threadIds) {
    try {
      const { data } = await client.post(`/threads/${threadId}/messages`, {
        text: messageText
      });

      results.push({ threadId, success: true, messageId: data.message.id });

      // Rate limiting: wait between sends
      await new Promise(resolve => setTimeout(resolve, 1000));

    } catch (error) {
      results.push({ threadId, success: false, error: error.message });
    }
  }

  const successful = results.filter(r => r.success).length;
  console.log(`Sent ${successful}/${threadIds.length} messages`);

  return results;
}
When sending bulk messages, add delays between requests to avoid rate limiting. A 1-second delay is recommended.

Message Content Guidelines

Character Limits

X (Twitter) DM limits apply:
  • Standard messages: 10,000 characters
  • Test your content stays within limits
function validateMessageLength(text: string): boolean {
  if (text.length > 10000) {
    throw new Error(`Message too long: ${text.length} characters (max 10,000)`);
  }
  return true;
}

Formatting

Messages support plain text. Special characters and emojis are supported:
await client.post(`/threads/${threadId}/messages`, {
  text: 'Hi! 👋 Thanks for reaching out. Here are our options:\n\n✅ Option A\n✅ Option B'
});
URLs in messages are automatically linkified on the platform:
await client.post(`/threads/${threadId}/messages`, {
  text: 'Check out our demo: https://example.com/demo'
});

Error Handling

Common Errors

Thread not found:
try {
  await client.post(`/threads/${threadId}/messages`, {
    text: 'Hello'
  });
} catch (error) {
  if (error.response?.status === 404) {
    console.error('Thread does not exist');
  }
}
Rate limited:
try {
  await client.post(`/threads/${threadId}/messages`, {
    text: 'Hello'
  });
} catch (error) {
  if (error.response?.status === 429) {
    console.error('Rate limit exceeded. Wait before retrying.');
    // Implement exponential backoff
  }
}
Empty message:
// Validate before sending
if (!text.trim()) {
  throw new Error('Message text cannot be empty');
}

await client.post(`/threads/${threadId}/messages`, { text });

Best Practices

Check for empty messages, length limits, and appropriate content before sending.
function validateMessage(text: string) {
  if (!text?.trim()) throw new Error('Empty message');
  if (text.length > 10000) throw new Error('Message too long');
  return true;
}
Implement delays and retry logic for bulk operations:
async function sendWithRetry(threadId: string, text: string, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await client.post(`/threads/${threadId}/messages`, { text });
    } catch (error) {
      if (error.response?.status === 429 && i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, 2000 * (i + 1)));
      } else {
        throw error;
      }
    }
  }
}
When you don’t need thread details, quick send reduces API calls and complexity.
Don’t assume all messages fit in one page. Always implement pagination for threads with many messages.

Next Steps