Skip to main content

Target Audience Discovery

Finding the right people on X (Twitter) at scale is one of the highest-value applications of the Sorsa API. This page covers six discovery techniques, each using a different endpoint to answer a different targeting question. Use them individually or combine them into a single workflow.
Note: For a deeper, narrative walkthrough with combined workflows and decision criteria, see How to Find Your Target Audience on Twitter Using the API on the blog.

The Six Techniques

#QuestionEndpointItems per request
1Who identifies as my target persona?/search-users~20
2Who already follows an account in my space?/followersup to 200
3Who joined a Community around my topic?/community-members~20
4Who is actively discussing my topic now?/search-tweets~20
5Which verified accounts follow a target?/verified-followersup to 200
6Who amplifies content in my space?/retweeters, /quotes~20
All examples use https://api.sorsa.io/v3 as the base URL and require the ApiKey header. See Authentication and Pagination for shared mechanics.
Endpoint: POST /v3/search-users Searches user bios, display names, and handles for a keyword. Use when the target audience self-declares with a specific role, title, or label.

Request

{
  "query": "Product Manager",
  "next_cursor": ""
}

Parameters

ParameterTypeRequiredDescription
querystringYesKeyword matched against bio, display name, handle.
next_cursorstringNoPagination cursor.

Python

import requests
import time

API_KEY = "YOUR_API_KEY"
URL = "https://api.sorsa.io/v3/search-users"

def find_users_by_bio(query, max_pages=10):
    all_users = []
    next_cursor = None

    for _ in range(max_pages):
        body = {"query": query}
        if next_cursor:
            body["next_cursor"] = next_cursor

        resp = requests.post(
            URL,
            headers={"ApiKey": API_KEY, "Content-Type": "application/json"},
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        all_users.extend(data.get("users", []))
        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break
        time.sleep(0.1)

    return all_users


users = find_users_by_bio("machine learning engineer", max_pages=10)

JavaScript

const API_KEY = "YOUR_API_KEY";

async function findUsersByBio(query, maxPages = 10) {
  const all = [];
  let cursor = null;

  for (let i = 0; i < maxPages; i++) {
    const body = { query };
    if (cursor) body.next_cursor = cursor;

    const resp = await fetch("https://api.sorsa.io/v3/search-users", {
      method: "POST",
      headers: { ApiKey: API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);

    const data = await resp.json();
    all.push(...(data.users || []));
    cursor = data.next_cursor;
    if (!cursor) break;
    await new Promise((r) => setTimeout(r, 100));
  }
  return all;
}

Post-filtering

Each response includes the full profile. Filter in code rather than running more queries:
qualified = [
    u for u in users
    if u.get("followers_count", 0) >= 1000
    and u.get("tweets_count", 0) >= 100
    and not u.get("protected", False)
]

Technique 2: Competitor Follower Extraction

Endpoint: GET /v3/followers Returns the follower list of any public account, up to 200 profiles per request. The highest-yield audience endpoint on Sorsa.

Request

GET https://api.sorsa.io/v3/followers?username=competitor_handle

Parameters

ParameterTypeRequiredDescription
usernamestringOne of threeHandle without @.
user_idstringOne of threeNumeric user ID.
user_linkstringOne of threeFull profile URL.
next_cursorstringNoPagination cursor.

Python

def get_followers(username, max_pages=10):
    all_followers = []
    cursor = None

    for _ in range(max_pages):
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor

        resp = requests.get(
            "https://api.sorsa.io/v3/followers",
            headers={"ApiKey": API_KEY},
            params=params,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        all_followers.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)

    return all_followers


followers = get_followers("competitor_handle", max_pages=20)
For a single-technique deep dive with overlap analysis, see Followers and Following.

Overlap analysis across multiple seeds

from collections import Counter

competitors = ["competitor_a", "competitor_b", "competitor_c"]
follower_sets = {}

for handle in competitors:
    fol = get_followers(handle, max_pages=10)
    follower_sets[handle] = {f["id"] for f in fol}

all_ids = [uid for ids in follower_sets.values() for uid in ids]
counts = Counter(all_ids)
overlap = {uid for uid, c in counts.items() if c >= 2}

Technique 3: Community Member Scraping

Endpoint: POST /v3/community-members Extracts the membership roster of an X Community. Members are pre-filtered by self-selected interest.

Request

{
  "community_link": "1966045657589813686",
  "next_cursor": ""
}
community_link accepts either the numeric ID or the full URL (https://x.com/i/communities/...).

Python

def get_community_members(community_id, max_pages=20):
    members = []
    next_cursor = None

    for _ in range(max_pages):
        body = {"community_link": community_id}
        if next_cursor:
            body["next_cursor"] = next_cursor

        resp = requests.post(
            "https://api.sorsa.io/v3/community-members",
            headers={"ApiKey": API_KEY, "Content-Type": "application/json"},
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        members.extend(data.get("users", []))
        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break
        time.sleep(0.1)

    return members
The response returns compact community profiles (id, username, display_name, profile image, verified, protected). To enrich with full profile data, pass the collected IDs through /info-batch (up to 100 IDs per call). See Lists and Communities for related endpoints.

Technique 4: Intent-Based Tweet Mining

Endpoint: POST /v3/search-tweets Searches tweet content for keywords, hashtags, and operator-driven queries. Extracts the unique authors as a real-time-intent audience.

Python

def find_active_voices(query, min_followers=100, max_pages=10):
    seen = set()
    voices = []
    next_cursor = None

    for _ in range(max_pages):
        body = {"query": query, "order": "latest"}
        if next_cursor:
            body["next_cursor"] = next_cursor

        resp = requests.post(
            "https://api.sorsa.io/v3/search-tweets",
            headers={"ApiKey": API_KEY, "Content-Type": "application/json"},
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        for tw in data.get("tweets", []):
            u = tw["user"]
            if u["id"] in seen:
                continue
            if u.get("followers_count", 0) < min_followers:
                continue
            seen.add(u["id"])
            voices.append({
                "username": u["username"],
                "followers": u.get("followers_count", 0),
                "bio": u.get("description", ""),
                "sample_tweet": tw["full_text"][:160],
            })

        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break
        time.sleep(0.1)

    return voices


voices = find_active_voices(
    '("need a CRM" OR "looking for a CRM") lang:en -filter:retweets',
    min_followers=100,
    max_pages=10,
)

Common query patterns

GoalQuery
Buying intent for a category"need a [category]" OR "looking for [category]" lang:en -filter:retweets
Competitor frustration"[competitor]" (frustrated OR broken OR "switching from") -from:[competitor]
Migration intent"migrating from [tool]" OR "switching from [tool]" lang:en
Recommendation requests("any recommendation" OR "anyone use") [topic] lang:en
Pain-point discussion"struggling with" OR "how do you handle" [topic] lang:en
For full operator syntax, see Search Operators. For tweet search reference, see Search Tweets.

Technique 5: Verified Follower Analysis

Endpoint: GET /v3/verified-followers Returns only verified followers of an account. Same shape and pagination as /followers, filtered to verified handles.

Python

def get_verified_followers(username, max_pages=10):
    verified = []
    cursor = None

    for _ in range(max_pages):
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor

        resp = requests.get(
            "https://api.sorsa.io/v3/verified-followers",
            headers={"ApiKey": API_KEY},
            params=params,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        verified.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)

    return verified


verified = get_verified_followers("openai", max_pages=10)
verified.sort(key=lambda u: u.get("followers_count", 0), reverse=True)

Technique 6: Retweeters and Quoters

Endpoints: POST /v3/retweeters, POST /v3/quotes Returns the users who amplified a specific tweet. /retweeters returns the user list. /quotes returns full quote tweets, which include the quoting user plus their commentary, useful for sentiment-tagged outreach.

Python

def get_retweeters(tweet_link, max_pages=10):
    users = []
    next_cursor = None

    for _ in range(max_pages):
        body = {"tweet_link": tweet_link}
        if next_cursor:
            body["next_cursor"] = next_cursor

        resp = requests.post(
            "https://api.sorsa.io/v3/retweeters",
            headers={"ApiKey": API_KEY, "Content-Type": "application/json"},
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        users.extend(data.get("users", []))

        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break
        time.sleep(0.1)

    return users


def get_quoters(tweet_link, max_pages=10):
    quotes = []
    next_cursor = None

    for _ in range(max_pages):
        body = {"tweet_link": tweet_link}
        if next_cursor:
            body["next_cursor"] = next_cursor

        resp = requests.post(
            "https://api.sorsa.io/v3/quotes",
            headers={"ApiKey": API_KEY, "Content-Type": "application/json"},
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        quotes.extend(data.get("tweets", []))

        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break
        time.sleep(0.1)

    return quotes
Quote tweets are tweet objects, not user objects. Access the quoting user via tweet["user"] and their commentary via tweet["full_text"].

Combining Techniques

Each technique covers a different blind spot. The typical pattern is to run several in parallel, then deduplicate and score by source-count:
from collections import defaultdict

def score_by_source(by_source):
    """
    by_source: dict of {source_name: list of user objects}
    Returns deduplicated users with a `source_count` field.
    """
    index = {}
    for source, users in by_source.items():
        for u in users:
            uid = u["id"]
            if uid not in index:
                index[uid] = {"user": u, "sources": set()}
            index[uid]["sources"].add(source)

    result = []
    for uid, entry in index.items():
        u = entry["user"].copy()
        u["source_count"] = len(entry["sources"])
        u["sources"] = sorted(entry["sources"])
        result.append(u)

    result.sort(key=lambda r: (-r["source_count"], -r.get("followers_count", 0)))
    return result


combined = score_by_source({
    "followers_competitor_a": followers_a,
    "bio_founder": bio_results,
    "intent_crm": intent_voices,
    "community_indie_hackers": community_members,
})
Users that appear in 2+ sources are typically the highest-value segment.

Filtering for Quality

A reusable filter to remove bots, inactive accounts, and low-signal profiles:
from datetime import datetime, timezone, timedelta

def is_quality_account(user, min_followers=500, min_tweets=100, max_following_ratio=10):
    if user.get("protected", False):
        return False
    if user.get("followers_count", 0) < min_followers:
        return False
    if user.get("tweets_count", 0) < min_tweets:
        return False

    followers = user.get("followers_count", 1)
    following = user.get("followings_count", 0)
    if following > followers * max_following_ratio:
        return False

    created = user.get("created_at")
    if created:
        try:
            dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
            if dt > datetime.now(timezone.utc) - timedelta(days=30):
                return False
        except ValueError:
            pass

    if not (user.get("description") or "").strip():
        return False

    return True


qualified = [u for u in users if is_quality_account(u)]

Exporting to CSV

Works with the output of any technique above:
import csv

def export_users_to_csv(users, output_file="audience.csv"):
    fields = [
        "user_id", "username", "display_name", "description",
        "followers_count", "followings_count", "tweets_count",
        "location", "verified", "created_at",
    ]

    with open(output_file, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fields)
        writer.writeheader()
        for u in users:
            writer.writerow({
                "user_id": u.get("id", ""),
                "username": u.get("username", ""),
                "display_name": u.get("display_name", ""),
                "description": (u.get("description") or "").replace("\n", " "),
                "followers_count": u.get("followers_count", 0),
                "followings_count": u.get("followings_count", 0),
                "tweets_count": u.get("tweets_count", 0),
                "location": u.get("location", ""),
                "verified": u.get("verified", False),
                "created_at": u.get("created_at", ""),
            })

Next Steps