> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sorsa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Descubrimiento del público objetivo

> Encuentra cuentas relevantes de X mediante perfiles, seguidores, comunidades e interacción con publicaciones.

Encuentra personas relevantes en X (Twitter) con seis técnicas. Cada una parte de una señal distinta: palabras clave del perfil, seguidores, pertenencia a comunidades, publicaciones recientes, verificación o interacción con una publicación concreta. Combina los resultados por ID de usuario para crear una audiencia sin duplicados.

Consulta una explicación paso a paso en [Cómo encontrar tu público objetivo en Twitter mediante API](https://api.sorsa.io/blog/twitter-audience-discovery).

## Elige una técnica

| Pregunta                                                          | Endpoint                           | Resultado                                              |
| :---------------------------------------------------------------- | :--------------------------------- | :----------------------------------------------------- |
| ¿Quién describe su perfil con un cargo o palabra clave relevante? | `POST /search-users`               | Perfiles de usuario                                    |
| ¿Quién sigue una cuenta de mi sector?                             | `GET /followers`                   | Hasta 200 perfiles por página                          |
| ¿Quién pertenece a una comunidad sobre mi tema?                   | `POST /community-members`          | Perfiles compactos de miembros                         |
| ¿Quién habla sobre mi tema?                                       | `POST /search-tweets`              | Hasta 20 publicaciones por página con sus autores      |
| ¿Qué cuentas verificadas siguen a una cuenta objetivo?            | `GET /verified-followers`          | Hasta 200 perfiles por página                          |
| ¿Quién amplifica una publicación?                                 | `POST /retweeters`, `POST /quotes` | Perfiles de quienes retuitean o publicaciones con cita |

El tamaño de página varía. Continúa con `next_cursor`: una página corta no significa que hayas llegado al final.

## Configuración y paginación compartida

Todos los ejemplos utilizan `https://api.sorsa.io/v3` y requieren `ApiKey`. Ejecuta los ejemplos de Python en el mismo script después de esta configuración. Instala `requests` con `python -m pip install requests` y configura la variable `SORSA_API_KEY`. JavaScript requiere un entorno de servidor con `fetch`, como Node.js 18 o posterior.

```python theme={null}
import os
import time
import requests

API_KEY = os.environ["SORSA_API_KEY"]
BASE_URL = "https://api.sorsa.io/v3"

def fetch_pages(method, endpoint, payload, result_key, max_pages=10):
    """Fetch a bounded number of pages; raise on HTTP errors."""
    items = []
    cursor = None
    seen_cursors = set()

    for _ in range(max_pages):
        values = dict(payload)
        if cursor:
            values["next_cursor"] = cursor
        options = {"params": values} if method == "GET" else {"json": values}
        response = requests.request(
            method, f"{BASE_URL}{endpoint}",
            headers={"ApiKey": API_KEY}, timeout=30, **options,
        )
        response.raise_for_status()
        data = response.json()
        items.extend(data.get(result_key) or [])
        cursor = data.get("next_cursor")
        if not cursor:
            break
        if cursor in seen_cursors:
            raise RuntimeError("Pagination returned a repeated cursor")
        seen_cursors.add(cursor)
        time.sleep(0.1)

    return items
```

`max_pages` limita el consumo; al alcanzarlo pueden quedar resultados sin leer. Estos ejemplos se detienen ante errores HTTP. En producción, añade reintentos limitados para `429` y errores transitorios siguiendo [Códigos de error](https://docs.sorsa.io/es/error-codes), y coordina los procesos con una clave compartida según los [límites de solicitudes](https://docs.sorsa.io/es/rate-limits). Consulta también [Autenticación](https://docs.sorsa.io/es/authentication) y [Paginación](https://docs.sorsa.io/es/pagination).

## Técnica 1: Buscar palabras clave en biografías

**Endpoint:** `POST /v3/search-users`

Busca cuentas por palabras o frases, como un cargo, profesión o interés. Revisa la biografía, el nombre público y el nombre de usuario para decidir si cada resultado encaja con tu audiencia.

```json theme={null}
{
  "query": "Product Manager"
}
```

| Parámetro     | Tipo   | Obligatorio | Descripción                                                       |
| :------------ | :----- | :---------- | :---------------------------------------------------------------- |
| `query`       | string | Sí          | Palabra clave o frase.                                            |
| `next_cursor` | string | No          | Cursor de la respuesta anterior; omítelo en la primera solicitud. |

### Python

```python theme={null}
def find_users_by_bio(query, max_pages=10):
    return fetch_pages("POST", "/search-users", {"query": query}, "users", max_pages)

bio_results = find_users_by_bio("machine learning engineer")
qualified = [
    u for u in bio_results
    if (u.get("followers_count") or 0) >= 1000
    and (u.get("tweets_count") or 0) >= 100
    and not u.get("protected", False)
]
```

### JavaScript

```javascript theme={null}
const API_KEY = process.env.SORSA_API_KEY;
if (!API_KEY) throw new Error("Set SORSA_API_KEY before running this example");

async function findUsersByBio(query, maxPages = 10) {
  const users = [];
  const seenCursors = new Set();
  let cursor = null;

  for (let i = 0; i < maxPages; i++) {
    const body = { query };
    if (cursor) body.next_cursor = cursor;
    const response = await fetch("https://api.sorsa.io/v3/search-users", {
      method: "POST",
      headers: { ApiKey: API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(30000),
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    users.push(...(data.users || []));
    cursor = data.next_cursor;
    if (!cursor) break;
    if (seenCursors.has(cursor)) throw new Error("Repeated pagination cursor");
    seenCursors.add(cursor);
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  return users;
}
```

## Técnica 2: Obtener seguidores de competidores

**Endpoint:** `GET /v3/followers`

Obtén hasta 200 seguidores por solicitud de una cuenta pública relevante. Proporciona `username` sin @, `user_id` como cadena o `user_link` como URL completa. Utiliza `next_cursor` para continuar.

```text theme={null}
GET https://api.sorsa.io/v3/followers?username=competitor_handle
```

```python theme={null}
def get_followers(username, max_pages=10):
    return fetch_pages("GET", "/followers", {"username": username}, "users", max_pages)

followers = get_followers("competitor_handle", max_pages=20)
```

### Coincidencias entre varias cuentas de partida

Cuenta cada usuario una sola vez por cuenta de partida. Sustituye los nombres de ejemplo antes de ejecutar:

```python theme={null}
from collections import Counter

competitors = ["competitor_a", "competitor_b", "competitor_c"]
follower_sets = {
    handle: {u["id"] for u in get_followers(handle, max_pages=10)}
    for handle in competitors
}
counts = Counter(uid for ids in follower_sets.values() for uid in ids)
overlap = {uid for uid, count in counts.items() if count >= 2}
```

Este cálculo mide coincidencias entre las páginas recuperadas, no necesariamente entre las listas completas. Consulta [Seguidores y cuentas seguidas](https://docs.sorsa.io/es/followers-and-following).

## Técnica 3: Descubrir miembros de comunidades

> **Comprueba primero la disponibilidad:** esta sección documenta el formato de las solicitudes de comunidades. Confirma la disponibilidad actual con [soporte](https://docs.sorsa.io/es/support) antes de incorporarlas a un flujo nuevo. Consulta [Listas y comunidades](https://docs.sorsa.io/es/lists-and-communities).

**Endpoint:** `POST /v3/community-members`

Obtén miembros de una comunidad de X. La pertenencia indica interés, pero no demuestra actividad reciente ni intención de compra.

```json theme={null}
{
  "community_link": "1966045657589813686"
}
```

`community_link` acepta el ID numérico como cadena o la URL completa.

```python theme={null}
def get_community_members(community_id, max_pages=20):
    return fetch_pages(
        "POST", "/community-members",
        {"community_link": community_id}, "users", max_pages,
    )
```

La respuesta contiene perfiles compactos: `id`, `username`, `display_name`, `profile_image_url`, `verified` y `protected`. Antes de filtrar por biografía o seguidores, completa los perfiles mediante [consulta de perfiles por lotes](https://docs.sorsa.io/es/api-reference/usuarios/perfiles-de-usuario-por-lotes), hasta 100 IDs por llamada. Consulta los endpoints relacionados en [Listas y comunidades](https://docs.sorsa.io/es/lists-and-communities).

## Técnica 4: Analizar publicaciones para identificar intención

**Endpoint:** `POST /v3/search-tweets`

Busca conversaciones recientes y extrae autores únicos. Conserva el objeto completo de cada usuario para combinarlo después con resultados de perfiles y seguidores.

```python theme={null}
def find_active_voices(query, min_followers=100, max_pages=10):
    tweets = fetch_pages(
        "POST", "/search-tweets",
        {"query": query, "order": "latest"}, "tweets", max_pages,
    )
    voices = {}
    for tweet in tweets:
        user = tweet.get("user")
        if not user or not user.get("id"):
            continue
        if (user.get("followers_count") or 0) < min_followers:
            continue
        if user["id"] not in voices:
            voices[user["id"]] = {
                **user,
                "sample_tweet": (tweet.get("full_text") or "")[:160],
            }
    return list(voices.values())

intent_voices = find_active_voices(
    '("need a CRM" OR "looking for a CRM") lang:en -filter:retweets',
)
```

### Patrones de consulta habituales

Sustituye los marcadores entre corchetes por tu categoría, cuenta, herramienta o tema. Los paréntesis aplican los filtros compartidos a ambos lados de `OR`. Estos ejemplos buscan expresiones en inglés.

| Objetivo                         | Consulta                                                                       |
| :------------------------------- | :----------------------------------------------------------------------------- |
| Intención de compra              | `("need a [category]" OR "looking for [category]") lang:en -filter:retweets`   |
| Insatisfacción con un competidor | `"[competitor]" (frustrated OR broken OR "switching from") -from:[competitor]` |
| Intención de migración           | `("migrating from [tool]" OR "switching from [tool]") lang:en`                 |
| Peticiones de recomendaciones    | `("any recommendation" OR "anyone use") [topic] lang:en`                       |
| Conversaciones sobre problemas   | `("struggling with" OR "how do you handle") [topic] lang:en`                   |

Añade `since:` y `until:` para delimitar el periodo. Revisa las publicaciones antes de interpretar una coincidencia como intención de compra. Consulta [Operadores de búsqueda](https://docs.sorsa.io/es/search-operators) y [Búsqueda de publicaciones](https://docs.sorsa.io/es/search-tweets).

## Técnica 5: Analizar seguidores verificados

**Endpoint:** `GET /v3/verified-followers`

Utiliza los mismos identificadores y paginación que en `/followers`. La verificación es un atributo de segmentación; evalúa la relevancia por separado.

```python theme={null}
def get_verified_followers(username, max_pages=10):
    return fetch_pages(
        "GET", "/verified-followers", {"username": username}, "users", max_pages,
    )

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

## Técnica 6: Usuarios que retuitean o citan

**Endpoints:** `POST /v3/retweeters`, `POST /v3/quotes`

`/retweeters` devuelve perfiles. `/quotes` devuelve publicaciones con el autor en `user` y su comentario en `full_text`.

```python theme={null}
def get_retweeters(tweet_link, max_pages=10):
    return fetch_pages(
        "POST", "/retweeters", {"tweet_link": tweet_link}, "users", max_pages,
    )

def get_quoters(tweet_link, max_pages=10):
    quote_tweets = fetch_pages(
        "POST", "/quotes", {"tweet_link": tweet_link}, "tweets", max_pages,
    )
    return list({
        tweet["user"]["id"]: tweet["user"]
        for tweet in quote_tweets if tweet.get("user")
    }.values())
```

`get_quoters` convierte las citas en perfiles únicos para el siguiente paso. Si necesitas sus comentarios, conserva `quote_tweets` y analiza `full_text` antes de convertirlos.

## Combinar las técnicas

Combina las listas por ID de cadena y conserva las fuentes de cada cuenta. Aparecer en más fuentes permite priorizar cuentas, pero no constituye una puntuación de confianza.

```python theme={null}
def score_by_source(by_source):
    index = {}
    for source, users in by_source.items():
        for user in users:
            uid = user["id"]
            if uid not in index:
                index[uid] = {"user": dict(user), "sources": set()}
            else:
                # Fill gaps when one source returns a compact profile.
                for field, value in user.items():
                    if index[uid]["user"].get(field) is None and value is not None:
                        index[uid]["user"][field] = value
            index[uid]["sources"].add(source)

    result = [
        {**entry["user"], "source_count": len(entry["sources"]),
         "sources": sorted(entry["sources"])}
        for entry in index.values()
    ]
    return sorted(result, key=lambda u: (-u["source_count"], -(u.get("followers_count") or 0)))

# Uses the results from Techniques 1, 2, and 4 above.
combined = score_by_source({
    "profile_search": bio_results,
    "competitor_followers": followers,
    "topic_discussion": intent_voices,
})
```

Añade miembros de comunidades después de completar sus perfiles. También puedes incorporar las listas de `get_retweeters` y `get_quoters`.

## Filtrar por calidad

Define criterios explícitos para tu proyecto. Este filtro comprueba integridad del perfil, antigüedad y contadores básicos. No detecta bots ni demuestra actividad reciente; revisa publicaciones recientes cuando la actividad importe.

```python theme={null}
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
    followers = user.get("followers_count") or 0
    if followers < min_followers or (user.get("tweets_count") or 0) < min_tweets:
        return False
    if (user.get("followings_count") or 0) > followers * max_following_ratio:
        return False
    if not (user.get("description") or "").strip():
        return False

    created = user.get("created_at")
    if not created:
        return False
    try:
        dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
        if dt.tzinfo is None:
            return False
    except (TypeError, ValueError):
        return False
    return dt <= datetime.now(timezone.utc) - timedelta(days=30)

qualified = [user for user in combined if is_quality_account(user)]
```

El ejemplo excluye cuentas con fechas de creación ausentes o no interpretables. Ajusta esa política y los umbrales a tu caso.

## Exportar a CSV

Exporta los usuarios después de eliminar duplicados y filtrar. Convierte primero los resultados de publicaciones a sus objetos `user` y completa los perfiles compactos si necesitas campos ausentes.

```python theme={null}
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 file:
        writer = csv.DictWriter(file, fieldnames=fields)
        writer.writeheader()
        for user in users:
            row = {field: user.get(field, "") for field in fields}
            row["user_id"] = user["id"]
            row["description"] = (user.get("description") or "").replace("\n", " ")
            writer.writerow(row)

export_users_to_csv(qualified)
```

Los valores ausentes quedan vacíos, no se convierten en cero. Al importar el CSV en una hoja de cálculo, configura `user_id` como texto para conservar el ID completo.

## Próximos pasos

* [Búsqueda de publicaciones](https://docs.sorsa.io/es/search-tweets): parámetros y ejemplos.
* [Operadores de búsqueda](https://docs.sorsa.io/es/search-operators): lógica booleana y filtros.
* [Seguidores y cuentas seguidas](https://docs.sorsa.io/es/followers-and-following): análisis de seguidores.
* [Listas y comunidades](https://docs.sorsa.io/es/lists-and-communities): miembros y publicaciones.
* [Análisis de competidores](https://docs.sorsa.io/es/Competitor-Analysis): flujos de análisis competitivo.
* [Supervisión en tiempo real](https://docs.sorsa.io/es/real-time-monitoring): consultas periódicas y deduplicación.
* [Seguimiento de menciones](https://docs.sorsa.io/es/search-mentions): marcas y competidores.
* [Optimización del uso de la API](https://docs.sorsa.io/es/optimizing-api-usage): lotes y presupuestos.
* [Referencia de la API](https://docs.sorsa.io/es/api-reference-guide): especificaciones.
