Skip to main content
GET
/
v1
/
campaigns
/
{campaign_id}
/
threads
/
{thread_id}
/
messages
Get Thread Messages
curl --request GET \
  --url https://api.example.com/v1/campaigns/{campaign_id}/threads/{thread_id}/messages \
  --header 'X-API-Key: <x-api-key>'
{
  "success": true,
  "messages": [
    {
      "message_id": "<string>",
      "thread_id": "<string>",
      "phone_number": "<string>",
      "sender": "<string>",
      "message": "<string>",
      "timestamp": "<string>",
      "media_url": "<string>",
      "sent_by_user_id": "<string>"
    }
  ],
  "thread_id": "<string>",
  "campaign_id": "<string>",
  "count": 123,
  "has_more": true,
  "powered_by": "<string>"
}

Description

Returns messages for a single thread scoped to a campaign. This endpoint is the fast, per-thread counterpart to Get Campaign Messages — it queries one thread directly instead of fanning out across every contact in the campaign. Use it for two patterns:
  • Polling for new messages — pass since with the timestamp of the latest message you already have.
  • Loading older messages (back-pagination) — pass before with the timestamp of the oldest message currently displayed.
Results are returned in reverse-chronological order (newest first).

Authentication

X-API-Key
string
required
Your Teli API key

Path Parameters

campaign_id
string
required
The campaign ID
thread_id
string
required
Thread identifier (format: {phone_number}_{campaign_id})

Query Parameters

since
string
ISO-8601 timestamp. Returns only messages with a timestamp strictly greater than this value. Use this for polling.
before
string
ISO-8601 timestamp. Returns only messages with a timestamp strictly less than this value. Use this to load older messages when paginating backward through a long conversation.
limit
integer
default:"50"
Maximum number of messages to return in this response.
since and before are mutually exclusive. If both are provided, since takes precedence and before is ignored.

Response Fields

success
boolean
true when the request succeeded
messages
array
Array of message objects, ordered newest-first
thread_id
string
Echoes the requested thread id
campaign_id
string
Echoes the requested campaign id
count
integer
Number of messages in this response
has_more
boolean
true when count equals limit — there may be additional older messages. Paginate by calling again with before set to the timestamp of the oldest message returned. false when fewer than limit rows were returned (end of thread).
powered_by
string
Always returns “Teli”

Example Requests

Initial load (newest messages)

cURL
curl "https://api.teli.ai/v1/campaigns/camp_abc123/threads/+15551234567_camp_abc123/messages?limit=50" \
  -H "X-API-Key: YOUR_API_KEY"

Poll for new messages since last seen

cURL
curl "https://api.teli.ai/v1/campaigns/camp_abc123/threads/+15551234567_camp_abc123/messages?since=2026-04-22T18:11:27.014Z" \
  -H "X-API-Key: YOUR_API_KEY"

Load older messages (back-pagination)

cURL
curl "https://api.teli.ai/v1/campaigns/camp_abc123/threads/+15551234567_camp_abc123/messages?before=2026-04-22T14:02:00.000Z&limit=50" \
  -H "X-API-Key: YOUR_API_KEY"
JavaScript
// Initial load
const initial = await fetch(
  `https://api.teli.ai/v1/campaigns/${campaignId}/threads/${encodeURIComponent(threadId)}/messages?limit=50`,
  { headers: { 'X-API-Key': 'YOUR_API_KEY' } }
).then(r => r.json());

// Poll for newer messages
const latestTs = initial.messages[0]?.timestamp;
const newer = await fetch(
  `https://api.teli.ai/v1/campaigns/${campaignId}/threads/${encodeURIComponent(threadId)}/messages?since=${encodeURIComponent(latestTs)}`,
  { headers: { 'X-API-Key': 'YOUR_API_KEY' } }
).then(r => r.json());

// Load older page
const oldestTs = initial.messages[initial.messages.length - 1]?.timestamp;
const older = await fetch(
  `https://api.teli.ai/v1/campaigns/${campaignId}/threads/${encodeURIComponent(threadId)}/messages?before=${encodeURIComponent(oldestTs)}&limit=50`,
  { headers: { 'X-API-Key': 'YOUR_API_KEY' } }
).then(r => r.json());
Python
import requests
from urllib.parse import quote

BASE = "https://api.teli.ai"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}

campaign_id = "camp_abc123"
thread_id = "+15551234567_camp_abc123"

# Initial load
r = requests.get(
    f"{BASE}/v1/campaigns/{campaign_id}/threads/{quote(thread_id)}/messages",
    params={"limit": 50},
    headers=HEADERS,
)
page = r.json()

# Back-paginate while has_more
while page.get("has_more"):
    oldest = page["messages"][-1]["timestamp"]
    r = requests.get(
        f"{BASE}/v1/campaigns/{campaign_id}/threads/{quote(thread_id)}/messages",
        params={"before": oldest, "limit": 50},
        headers=HEADERS,
    )
    page = r.json()

Example Responses

Success

200
{
  "success": true,
  "messages": [
    {
      "message_id": "conv_9f1b2e7a",
      "thread_id": "+15551234567_camp_abc123",
      "phone_number": "+15551234567",
      "sender": "contact",
      "message": "Yeah, send me the details.",
      "timestamp": "2026-04-22T18:12:03.221Z",
      "media_url": null
    },
    {
      "message_id": "conv_7cda8102",
      "thread_id": "+15551234567_camp_abc123",
      "phone_number": "+15551234567",
      "sender": "human",
      "message": "Hi Jane — are you still looking at the Maple Ave listing?",
      "timestamp": "2026-04-22T18:11:27.014Z",
      "media_url": null,
      "sent_by_user_id": "user_xyz789"
    }
  ],
  "thread_id": "+15551234567_camp_abc123",
  "campaign_id": "camp_abc123",
  "count": 2,
  "has_more": false,
  "powered_by": "Teli"
}

No new messages (polling with since)

200
{
  "success": true,
  "messages": [],
  "thread_id": "+15551234567_camp_abc123",
  "campaign_id": "camp_abc123",
  "count": 0,
  "has_more": false,
  "powered_by": "Teli"
}

Server error

500
{
  "error": "Failed to get thread messages",
  "powered_by": "Teli"
}

Behavior

  • Single-thread scope — only messages whose thread_id exactly matches the path parameter are returned. Much faster than the campaign-wide messages endpoint for UI rendering of a single conversation.
  • Reverse-chronological — messages are returned newest first. When back-paginating with before, each page is also newest-first within that page.
  • has_more semanticshas_more is true whenever count == limit. It is a hint, not a guarantee: the next page may return zero rows if the thread ended exactly at the page boundary.
  • Sender field — distinguishes between automated sends (agent, drip, broadcast), human operator sends (human), and inbound messages from the contact (contact). Use this to style outbound vs inbound bubbles in a UI.
  • sent_by_user_id — only present on human sends. Use it to attribute outbound messages to the specific operator who sent them.