Skip to main content

Listing threads

Retrieve threads with filtering and pagination:
const { data } = await client.get("/threads", {
  params: {
    limit: 20,
    inbox: "default",
  },
});

console.log(`Found ${data.threads.length} threads`);
data.threads.forEach((thread) => {
  console.log(`${thread.id} — @${thread.prospect.username}`);
});
Response:
{
  "threads": [
    {
      "id": "l44e15irdq4db30i77cgphhx",
      "platform": "twitter",
      "done": false,
      "assigneeId": null,
      "lastMessageTimestamp": "2025-01-15T15:30:00.000Z",
      "computedSortTimestamp": "2025-01-15T15:30:00.000Z",
      "createdAt": "2025-01-10T10:00:00.000Z",
      "status": "active",
      "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx",
      "isSyncing": false,
      "isRequest": false,
      "lastMessage": {
        "id": "p8rvk2m5j0xn4wq7ybftcael",
        "content": "Sounds great, let's schedule a call!",
        "authorId": "hzcai5t59nn9vsck3rbuepyg",
        "createdAt": "2025-01-15T15:30:00.000Z",
        "userId": null,
        "campaignId": null
      },
      "prospect": {
        "platform": "twitter",
        "platformId": "1876543210987654321",
        "displayName": "Jordan Rivera",
        "username": "jordanrivera",
        "handle": "@jordanrivera",
        "image": "https://pbs.twimg.com/profile_images/.../photo.jpg",
        "bio": "Head of Growth at TechCo",
        "followerCount": 12400,
        "verified": "none",
        "context": {
          "tags": [],
          "statusId": null,
          "valuation": null,
          "notes": null
        },
        "..."
      }
    }
  ],
  "nextCursor": {
    "id": "l44e15irdq4db30i77cgphhx",
    "timestamp": "2025-01-15T15:30:00.000Z"
  }
}
The default page size is 20 (max 100). Use nextCursor to paginate.

Looking up threads by username

Find all threads for a prospect by their exact platform username:
const { data: threads } = await client.get("/threads/lookup-by-username", {
  params: {
    username: "jordanrivera",
  },
});

threads.forEach((thread) => {
  console.log(`${thread.id}${thread.accountLinkId}`);
});
Returns a flat array of Thread objects — one per account link that has a conversation with that prospect. No pagination; all matching threads are returned. Scope results to specific connected accounts:
const { data: threads } = await client.get("/threads/lookup-by-username", {
  params: {
    username: "jordanrivera",
    "accountLinkIds[0]": "df6jbw4h36qm5d9iu2sgn7kx",
  },
});
The username must be an exact match (case-insensitive, without the @ prefix). For lookup by platform ID, use GET /threads/lookup instead.

Filtering threads

By Inbox view

The inbox parameter filters by thread state:
// Active conversations
const active = await client.get("/threads", { params: { inbox: "default" } });

// Prospect sent the last message — you need to reply
const noReply = await client.get("/threads", { params: { inbox: "no-reply" } });

// New message requests
const requests = await client.get("/threads", {
  params: { inbox: "requests" },
});

// Archived (done)
const archived = await client.get("/threads", {
  params: { inbox: "archived" },
});
Filter threads to specific connected accounts. Pass an array of IDs via accountLinkIds:
const { data } = await client.get("/threads", {
  params: {
    "accountLinkIds[0]": "df6jbw4h36qm5d9iu2sgn7kx",
  },
});

By tags

// Prospects with ANY of these tags
const { data } = await client.get("/threads", {
  params: {
    "filters[tags][selectedIds][0]": "t8rvk2m5j0xn4wq7ybftcael",
    "filters[tags][selectedIds][1]": "u9swl3n6k1yo5xr8zcgudbfm",
  },
});

// Prospects with NO tags
const { data: untagged } = await client.get("/threads", {
  params: {
    "filters[tags][noTags]": true,
  },
});

By status

// Threads with prospects in specific statuses
const { data } = await client.get("/threads", {
  params: {
    "filters[statuses][selectedIds][0]": "s3km7xj9wq5p2bvnhfdteoly",
  },
});

// Prospects with no status
const { data: noStatus } = await client.get("/threads", {
  params: {
    "filters[statuses][noStatus]": true,
  },
});

By assignee

// Assigned to specific member
const { data } = await client.get("/threads", {
  params: {
    "filters[assignees][selectedIds][0]": "r3km7xj9wq5p2bvnhfdteoly",
  },
});

// Unassigned threads
const { data: unassigned } = await client.get("/threads", {
  params: {
    "filters[assignees][noAssignee]": true,
  },
});

By campaign

// Threads from specific campaigns
const { data } = await client.get("/threads", {
  params: {
    "filters[campaigns][selectedIds][0]": "c5nwp8r2j3ym6at0xbkqhfvg",
  },
});

// Threads not from any campaign
const { data: noCampaign } = await client.get("/threads", {
  params: {
    "filters[campaigns][noCampaign]": true,
  },
});

Sort order

const { data } = await client.get("/threads", {
  params: {
    "filters[sortOrder]": "desc", // or 'asc'
  },
});

Combined filters

Filters combine with AND logic:
// Active threads, qualified status, assigned to a specific member
const { data } = await client.get("/threads", {
  params: {
    inbox: "default",
    "accountLinkIds[0]": "df6jbw4h36qm5d9iu2sgn7kx",
    "filters[statuses][selectedIds][0]": "s3km7xj9wq5p2bvnhfdteoly",
    "filters[assignees][selectedIds][0]": "r3km7xj9wq5p2bvnhfdteoly",
    limit: 20,
  },
});

Filter helper

A utility to build filter params:
function buildFilters(opts: {
  tagIds?: string[];
  statusIds?: string[];
  assigneeIds?: string[];
  campaignIds?: string[];
  noTags?: boolean;
  noStatus?: boolean;
  noAssignee?: boolean;
  noCampaign?: boolean;
  sortOrder?: "asc" | "desc";
}) {
  const params: Record<string, string | boolean> = {};

  opts.tagIds?.forEach((id, i) => {
    params[`filters[tags][selectedIds][${i}]`] = id;
  });
  opts.statusIds?.forEach((id, i) => {
    params[`filters[statuses][selectedIds][${i}]`] = id;
  });
  opts.assigneeIds?.forEach((id, i) => {
    params[`filters[assignees][selectedIds][${i}]`] = id;
  });
  opts.campaignIds?.forEach((id, i) => {
    params[`filters[campaigns][selectedIds][${i}]`] = id;
  });

  if (opts.noTags) params["filters[tags][noTags]"] = true;
  if (opts.noStatus) params["filters[statuses][noStatus]"] = true;
  if (opts.noAssignee) params["filters[assignees][noAssignee]"] = true;
  if (opts.noCampaign) params["filters[campaigns][noCampaign]"] = true;
  if (opts.sortOrder) params["filters[sortOrder]"] = opts.sortOrder;

  return params;
}

const { data } = await client.get("/threads", {
  params: {
    inbox: "default",
    ...buildFilters({ tagIds: ["t8rvk2m5j0xn4wq7ybftcael"], noAssignee: true }),
    limit: 50,
  },
});

Pagination

Use cursorId and cursorTimestamp to paginate through all threads:
async function getAllThreads() {
  const allThreads: any[] = [];
  let cursorId: string | undefined;
  let cursorTimestamp: string | undefined;

  do {
    const { data } = await client.get("/threads", {
      params: {
        limit: 100,
        ...(cursorId && { cursorId, cursorTimestamp }),
      },
    });

    allThreads.push(...data.threads);
    cursorId = data.nextCursor?.id;
    cursorTimestamp = data.nextCursor?.timestamp;
  } while (cursorId);

  return allThreads;
}
See Pagination for streaming patterns and error handling.

Looking up a thread

Find an existing thread between an account and a prospect:
const { data: thread } = await client.get("/threads/lookup", {
  params: {
    externalPlatformId: "1876543210987654321",
    accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx",
  },
});

if (thread) {
  console.log("Found thread:", thread.id);
} else {
  console.log("No thread exists yet");
}
The lookup endpoint accepts either externalPlatformId (X user ID) or externalId (Inbox external ID). At least one must be provided along with accountLinkId. Returns the Thread object directly, or null if no thread exists.

Creating a thread

Create a new conversation thread:
const { data: thread } = await client.post("/threads", {
  externalPlatformId: "1876543210987654321",
  accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx",
});

console.log("Created thread:", thread.id);
Creating a thread doesn’t send a message. Use the messages endpoint to send a DM.
If a thread already exists between the account link and prospect, the API will return an error. Use lookup first to check, or use the lookup-or-create pattern below.

Getting thread details

const { data: thread } = await client.get(`/threads/${threadId}`);

console.log("Thread:", thread.id);
console.log("Prospect:", thread.prospect.displayName);
console.log("Last message:", thread.lastMessage?.content);
Returns the Thread object directly — not wrapped in { thread: ... }.

Updating threads

Modify thread assignment and done status:
// Assign thread to a team member
await client.patch(`/threads/${threadId}`, {
  assigneeId: "r3km7xj9wq5p2bvnhfdteoly",
});

// Mark thread as done (archives it)
await client.patch(`/threads/${threadId}`, {
  done: true,
});

// Unassign and reopen
await client.patch(`/threads/${threadId}`, {
  assigneeId: null,
  done: false,
});

Deleting threads

Soft-delete a thread:
await client.delete(`/threads/${threadId}`);
Deletion is permanent and cannot be undone. The thread is also deleted on the X platform. Consider using done: true to archive instead.

Common workflows

Lookup-or-create pattern

async function getOrCreateThread(
  prospectPlatformId: string,
  accountLinkId: string,
) {
  const { data: existing } = await client.get("/threads/lookup", {
    params: { externalPlatformId: prospectPlatformId, accountLinkId },
  });

  if (existing) return existing;

  const { data: created } = await client.post("/threads", {
    externalPlatformId: prospectPlatformId,
    accountLinkId,
  });

  return created;
}

Round-robin assignment

const memberIds = ["r3km7xj9wq5p2bvnhfdteoly", "s4ln8yk0xr6q3cw1behsifup", "t5mo9zl1ys7r4dx2cfitjgvqw"];
let idx = 0;

async function assignRoundRobin(threadId: string) {
  await client.patch(`/threads/${threadId}`, {
    assigneeId: memberIds[idx],
  });
  idx = (idx + 1) % memberIds.length;
}

Get threads needing follow-up

const { data } = await client.get("/threads", {
  params: {
    inbox: "no-reply",
    limit: 50,
  },
});

console.log(`${data.threads.length} threads awaiting your reply`);