> ## 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.

# Migración desde la API oficial de X

# Migrar de la API oficial de X v2 a Sorsa API v3

Esta referencia cubre autenticación, equivalencias de endpoints, respuestas, paginación, métodos HTTP, sintaxis de búsqueda, errores y ejemplos de cURL, Python y JavaScript.

Sorsa es de solo lectura. Si tu integración publica, envía mensajes, da Me gusta o sigue cuentas, conserva la API oficial para esas operaciones y migra solo las lecturas. Las 100 solicitudes gratuitas de cada cuenta, sin tarjeta, permiten validar los endpoints antes de migrar tráfico de producción.

> **Nota:** consulta costes y ejemplos paso a paso en la [guía completa de migración](https://api.sorsa.io/blog/migrate-from-twitter-api).

## Resumen de cambios

| Aspecto                | API oficial de X v2                         | Sorsa API v3                           |
| ---------------------- | ------------------------------------------- | -------------------------------------- |
| URL base               | `https://api.x.com/2`                       | `https://api.sorsa.io/v3`              |
| Autenticación          | OAuth 2.0 Bearer / OAuth 1.0a               | Clave de API en el encabezado `ApiKey` |
| Selección de campos    | `tweet.fields`, `user.fields`, `expansions` | Todos los campos por defecto           |
| Estructura contenedora | `data` + `includes` + `meta`                | Objeto plano con relaciones incluidas  |
| Paginación             | `pagination_token` / `meta.next_token`      | `next_cursor` (nivel superior)         |
| Límites de solicitudes | Por endpoint, en ventanas de 15 minutos     | 20 solicitudes por segundo para todos  |
| Formato de error       | `errors[]` con `type`, `title`, `detail`    | `{ "message": "..." }`                 |

## Autenticación

La API oficial utiliza OAuth 2.0 Bearer para solicitudes de aplicación y OAuth 1.0a User Context para solicitudes de usuario.

```bash theme={null}
# Official API (OAuth 2.0 App-Only)
curl "https://api.x.com/2/users/by/username/elonmusk" \
  -H "Authorization: Bearer $BEARER_TOKEN"
```

Sorsa utiliza una clave en el encabezado `ApiKey`. Genera las claves en el [panel](https://api.sorsa.io/overview/keys).

```bash theme={null}
curl "https://api.sorsa.io/v3/info?username=elonmusk" \
  -H "ApiKey: $API_KEY"
```

```python theme={null}
import requests

response = requests.get(
    "https://api.sorsa.io/v3/info",
    params={"username": "elonmusk"},
    headers={"ApiKey": API_KEY},
)
user = response.json()
```

```javascript theme={null}
const response = await fetch("https://api.sorsa.io/v3/info?username=elonmusk", {
  headers: { ApiKey: API_KEY },
});
const user = await response.json();
```

Consulta [Autenticación](https://docs.sorsa.io/es/authentication).

## Equivalencias de endpoints

### Usuarios

| Acción                       | API oficial de X v2                  | Sorsa API v3                                |
| ---------------------------- | ------------------------------------ | ------------------------------------------- |
| Consultar usuario por nombre | `GET /2/users/by/username/:username` | `GET /info?username=:username`              |
| Consultar usuario por ID     | `GET /2/users/:id`                   | `GET /info?user_id=:id`                     |
| Consultar varios usuarios    | `GET /2/users?ids=...`               | `GET /info-batch?user_ids=...&user_ids=...` |
| Obtener seguidores           | `GET /2/users/:id/followers`         | `GET /followers?user_id=:id`                |
| Obtener cuentas seguidas     | `GET /2/users/:id/following`         | `GET /follows?user_id=:id`                  |
| Seguidores verificados       | No disponible                        | `GET /verified-followers?user_id=:id`       |
| Metadatos About de la cuenta | No disponible                        | `GET /about?username=:username`             |

* `GET /info-batch` acepta hasta 100 nombres o IDs. Repite el parámetro: `?usernames=a&usernames=b`.
* `GET /followers` y `GET /follows` devuelven hasta **200 perfiles completos por página**, con biografía, contadores y verificación.

### Publicaciones

| Acción                         | API oficial de X v2              | Sorsa API v3                                               |
| ------------------------------ | -------------------------------- | ---------------------------------------------------------- |
| Consultar una publicación      | `GET /2/tweets/:id`              | `POST /tweet-info` cuerpo: `{ "tweet_link": ":id" }`       |
| Consultar varias publicaciones | `GET /2/tweets?ids=...`          | `POST /tweet-info-bulk` cuerpo: `{ "tweet_links": [...] }` |
| Cronología de usuario          | `GET /2/users/:id/tweets`        | `POST /user-tweets` cuerpo: `{ "user_id": ":id" }`         |
| Citas                          | `GET /2/tweets/:id/quote_tweets` | `POST /quotes` cuerpo: `{ "tweet_link": ":id" }`           |
| Usuarios que retuitearon       | `GET /2/tweets/:id/retweeted_by` | `POST /retweeters` cuerpo: `{ "tweet_link": ":id" }`       |
| Respuestas (comentarios)       | Sin endpoint específico          | `POST /comments` cuerpo: `{ "tweet_link": ":id" }`         |
| Artículo largo de X            | No disponible                    | `POST /article` cuerpo: `{ "tweet_link": ":id" }`          |

* `tweet_link` acepta una URL completa como `https://x.com/user/status/123` o el ID `"123"`.
* `POST /tweet-info-bulk` devuelve hasta 100 publicaciones por solicitud. Utilízalo en lugar de repetir `POST /tweet-info` para reducir hasta 100 veces las llamadas.
* `POST /user-tweets` no tiene un máximo de 3.200 publicaciones. Recorre `next_cursor` hasta que no aparezca para consultar toda la cronología. Consulta [Datos históricos](https://docs.sorsa.io/es/historical-data).

### Búsquedas

| Acción                         | API oficial de X v2                     | Sorsa API v3                                       |
| ------------------------------ | --------------------------------------- | -------------------------------------------------- |
| Buscar publicaciones recientes | `GET /2/tweets/search/recent?query=...` | `POST /search-tweets` cuerpo: `{ "query": "..." }` |
| Buscar en todo el archivo      | `GET /2/tweets/search/all?query=...`    | `POST /search-tweets` (incluye datos históricos)   |
| Buscar menciones               | `GET .../search/recent?query=@user`     | `POST /mentions` cuerpo: `{ "query": "user" }`     |
| Buscar usuarios                | No disponible en v2                     | `POST /search-users` cuerpo: `{ "query": "..." }`  |

* `/search-tweets` utiliza la sintaxis de búsqueda avanzada de la web de X. Muchos términos básicos se conservan, pero revisa los operadores específicos de API v2 antes de reutilizar consultas. Consulta [Operadores de búsqueda](https://docs.sorsa.io/es/search-operators).
* `POST /mentions` añade filtros como `min_likes`, `min_replies`, `min_retweets`, `since_date` y `until_date`.

### Listas

| Acción                     | API oficial de X v2          | Sorsa API v3                        |
| -------------------------- | ---------------------------- | ----------------------------------- |
| Miembros de una lista      | `GET /2/lists/:id/members`   | `GET /list-members?list_id=:id`     |
| Seguidores de una lista    | `GET /2/lists/:id/followers` | `GET /list-followers?list_link=:id` |
| Publicaciones de una lista | `GET /2/lists/:id/tweets`    | `GET /list-tweets?list_id=:id`      |

### Comunidades

La API oficial de X no expone estos endpoints de comunidades; son funciones de Sorsa.

| Acción                         | Sorsa API v3                                                                          |
| ------------------------------ | ------------------------------------------------------------------------------------- |
| Miembros de una comunidad      | `POST /community-members` cuerpo: `{ "community_link": ":id" }`                       |
| Publicaciones de una comunidad | `POST /community-tweets` cuerpo: `{ "community_id": ":id", "order": "popular" }`      |
| Buscar dentro de una comunidad | `POST /community-search-tweets` cuerpo: `{ "community_link": ":id", "query": "..." }` |

Consulta los parámetros y la nota de disponibilidad en [Listas y comunidades](https://docs.sorsa.io/es/lists-and-communities). Confirma la compatibilidad actual antes de migrar este tipo de flujo.

### Verificación

Estos endpoints responden a preguntas sobre acciones concretas. La API oficial no tiene un equivalente directo; replicarlos requiere recuperar listas y examinarlas en el cliente.

| Pregunta                                        | Sorsa API v3                                     |
| ----------------------------------------------- | ------------------------------------------------ |
| ¿El usuario A sigue al usuario B?               | `POST /check-follow`                             |
| ¿El usuario X comentó la publicación Y?         | `GET /check-comment?tweet_link=...&username=...` |
| ¿El usuario X citó o retuiteó la publicación Y? | `POST /check-quoted`                             |
| ¿El usuario X retuiteó la publicación Y?        | `POST /check-retweet`                            |
| ¿El usuario X pertenece a la comunidad Y?       | `POST /check-community-member`                   |

Consulta [Verificación de campañas](https://docs.sorsa.io/es/Marketing-Campaign-Verification).

### Análisis exclusivo de Sorsa

| Acción                                             | Sorsa API v3                         |
| -------------------------------------------------- | ------------------------------------ |
| Puntuación de influencia                           | `GET /score?username=...`            |
| Variaciones de puntuación (7 y 30 días)            | `GET /score-changes?username=...`    |
| Seguidores por categoría                           | `GET /followers-stats?username=...`  |
| Los 20 principales seguidores por puntuación       | `GET /top-followers?username=...`    |
| Las 20 principales cuentas seguidas por puntuación | `GET /top-following?username=...`    |
| Nuevos seguidores (7 días)                         | `GET /new-followers-7d?username=...` |
| Nuevas cuentas seguidas (7 días)                   | `GET /new-following-7d?username=...` |

Estos endpoints indexan un subconjunto de cuentas del sector de criptomonedas: influencers, proyectos y fondos. Consulta [Sorsa Score y análisis de criptomonedas](https://docs.sorsa.io/es/sorsa-score-and-crypto-analytics).

### Utilidades

| Acción                              | Sorsa API v3                  |
| ----------------------------------- | ----------------------------- |
| Nombre de usuario a ID numérico     | `GET /username-to-id/:handle` |
| ID numérico a nombre de usuario     | `GET /id-to-username/:id`     |
| URL de perfil a ID numérico         | `GET /link-to-id?link=...`    |
| Estadísticas de consumo de la clave | `GET /key-usage-info`         |

Consulta [Conversión de IDs](https://docs.sorsa.io/es/ID-Conversion).

## Cambios en las respuestas

Es uno de los cambios principales. La API oficial envuelve los resultados en `data`, `includes` y `meta`. Sorsa devuelve objetos planos con el autor incluido en cada publicación.

### Perfil de usuario

**API oficial de X v2**, con selección de campos:

```json theme={null}
{
  "data": {
    "id": "44196397",
    "name": "Elon Musk",
    "username": "elonmusk",
    "verified": false,
    "profile_image_url": "https://pbs.twimg.com/...",
    "public_metrics": {
      "followers_count": 100000000,
      "following_count": 500,
      "tweet_count": 30000,
      "listed_count": 12000
    }
  }
}
```

**Sorsa API v3:**

```json theme={null}
{
  "id": "44196397",
  "username": "elonmusk",
  "display_name": "Elon Musk",
  "description": "...",
  "location": "Austin, TX",
  "profile_image_url": "https://pbs.twimg.com/...",
  "profile_background_image_url": "...",
  "followers_count": 100000000,
  "followings_count": 500,
  "tweets_count": 30000,
  "favourites_count": 50000,
  "media_count": 1200,
  "verified": false,
  "protected": false,
  "can_dm": true,
  "possibly_sensitive": false,
  "created_at": "2009-06-02T20:12:29Z",
  "bio_urls": ["https://example.com"],
  "pinned_tweet_ids": ["17823..."]
}
```

### Publicación

**API oficial de X v2**, con `expansions=author_id`:

```json theme={null}
{
  "data": {
    "id": "1234567890",
    "text": "Hello world",
    "created_at": "2024-01-15T12:00:00.000Z",
    "author_id": "44196397",
    "conversation_id": "1234567890",
    "lang": "en",
    "public_metrics": {
      "retweet_count": 100,
      "reply_count": 50,
      "like_count": 500,
      "quote_count": 25,
      "bookmark_count": 10,
      "impression_count": 50000
    }
  },
  "includes": {
    "users": [
      { "id": "44196397", "name": "Elon Musk", "username": "elonmusk" }
    ]
  }
}
```

**Sorsa API v3:**

```json theme={null}
{
  "id": "1234567890",
  "full_text": "Hello world",
  "created_at": "2024-01-15T12:00:00Z",
  "lang": "en",
  "conversation_id_str": "1234567890",
  "likes_count": 500,
  "retweet_count": 100,
  "reply_count": 50,
  "quote_count": 25,
  "view_count": 50000,
  "bookmark_count": 10,
  "is_reply": false,
  "is_quote_status": false,
  "is_replies_limited": false,
  "in_reply_to_tweet_id": null,
  "in_reply_to_username": null,
  "user": {
    "id": "44196397",
    "username": "elonmusk",
    "display_name": "Elon Musk",
    "followers_count": 100000000
  },
  "entities": [],
  "quoted_status": null,
  "retweeted_status": null
}
```

### Equivalencias de campos

#### Campos de usuario

| API oficial de X v2              | Sorsa API v3                   | Notas                                        |
| -------------------------------- | ------------------------------ | -------------------------------------------- |
| `id`                             | `id`                           | Sin cambios                                  |
| `username`                       | `username`                     | Sin cambios                                  |
| `name`                           | `display_name`                 | Nombre modificado                            |
| `description`                    | `description`                  | Sin cambios                                  |
| `location`                       | `location`                     | Sin cambios                                  |
| `verified`                       | `verified`                     | Sin cambios                                  |
| `protected`                      | `protected`                    | Sin cambios                                  |
| `profile_image_url`              | `profile_image_url`            | Sin cambios                                  |
| `created_at`                     | `created_at`                   | Sin cambios                                  |
| `public_metrics.followers_count` | `followers_count`              | En el nivel superior                         |
| `public_metrics.following_count` | `followings_count`             | En el nivel superior y con nombre modificado |
| `public_metrics.tweet_count`     | `tweets_count`                 | En el nivel superior y con nombre modificado |
| `public_metrics.listed_count`    | No disponible                  |                                              |
| No disponible                    | `favourites_count`             | Solo Sorsa                                   |
| No disponible                    | `media_count`                  | Solo Sorsa                                   |
| No disponible                    | `can_dm`                       | Solo Sorsa                                   |
| No disponible                    | `bio_urls`                     | Solo Sorsa                                   |
| No disponible                    | `pinned_tweet_ids`             | Solo Sorsa                                   |
| No disponible                    | `profile_background_image_url` | Solo Sorsa                                   |
| No disponible                    | `possibly_sensitive`           | Solo Sorsa                                   |

#### Campos de publicación

| API oficial de X v2                                | Sorsa API v3                                        | Notas                                            |
| -------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------ |
| `id`                                               | `id`                                                | Sin cambios                                      |
| `text`                                             | `full_text`                                         | Nombre modificado                                |
| `created_at`                                       | `created_at`                                        | Sin cambios                                      |
| `lang`                                             | `lang`                                              | Sin cambios                                      |
| `conversation_id`                                  | `conversation_id_str`                               | Nombre modificado                                |
| `in_reply_to_user_id`                              | `in_reply_to_username`                              | Devuelve el nombre en lugar del ID numérico      |
| `public_metrics.like_count`                        | `likes_count`                                       | En el nivel superior, nombre modificado (plural) |
| `public_metrics.retweet_count`                     | `retweet_count`                                     | En el nivel superior                             |
| `public_metrics.reply_count`                       | `reply_count`                                       | En el nivel superior                             |
| `public_metrics.quote_count`                       | `quote_count`                                       | En el nivel superior                             |
| `public_metrics.bookmark_count`                    | `bookmark_count`                                    | En el nivel superior                             |
| `public_metrics.impression_count`                  | `view_count`                                        | En el nivel superior y con nombre modificado     |
| `author_id` + `includes.users[]`                   | `user` (objeto completo incluido)                   | Incluido en el objeto                            |
| Publicaciones referenciadas mediante `includes`    | `quoted_status`, `retweeted_status`                 | Objetos incluidos                                |
| No disponible                                      | `is_reply`, `is_quote_status`, `is_replies_limited` | Booleanos exclusivos de Sorsa                    |
| No disponible                                      | `in_reply_to_tweet_id`                              | Solo Sorsa                                       |
| `entities` (URL, menciones, hashtags y multimedia) | `entities` array de `{ type, link, preview }`       | Estructura diferente                             |

## Paginación

La API oficial envía `pagination_token` y devuelve `meta.next_token`. Sorsa utiliza `next_cursor` en ambas direcciones.

**En GET**, envíalo como parámetro de consulta:

```bash theme={null}
curl "https://api.sorsa.io/v3/followers?username=elonmusk&next_cursor=ABC123" \
  -H "ApiKey: $API_KEY"
```

**En POST**, inclúyelo en el cuerpo JSON:

```bash theme={null}
curl -X POST "https://api.sorsa.io/v3/search-tweets" \
  -H "ApiKey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "from:elonmusk", "next_cursor": "ABC123" }'
```

La respuesta lo devuelve en el nivel superior:

```json theme={null}
{
  "tweets": [...],
  "next_cursor": "XYZ789"
}
```

Si falta o es `null`, no quedan páginas. Consulta [Paginación](https://docs.sorsa.io/es/pagination).

## Diferencias de método HTTP

Algunos endpoints GET de la API oficial son POST en Sorsa.

| Acción                        | API oficial               | Sorsa API |
| ----------------------------- | ------------------------- | --------- |
| Consultar una publicación     | GET                       | **POST**  |
| Buscar publicaciones          | GET                       | **POST**  |
| Cronología de usuario         | GET                       | **POST**  |
| Citas                         | GET                       | **POST**  |
| Usuarios que retuitearon      | GET                       | **POST**  |
| Respuestas (comentarios)      | (sin equivalente directo) | **POST**  |
| Perfil de usuario             | GET                       | GET       |
| Seguidores y cuentas seguidas | GET                       | GET       |
| Listas                        | GET                       | GET       |

Como regla general, los endpoints de publicaciones, búsquedas y comunidades utilizan POST con JSON; esto incluye `/user-tweets` aunque reciba un usuario. Los de usuarios, listas y utilidades utilizan GET con parámetros de consulta o ruta. La excepción es `/check-comment`: usa GET aunque reciba un enlace de publicación. Comprueba siempre la referencia si tienes dudas.

## Ejemplos de migración de código

### Consultar un perfil

**Antes: API oficial**

```bash theme={null}
curl "https://api.x.com/2/users/by/username/elonmusk?user.fields=description,public_metrics,profile_image_url,verified,created_at" \
  -H "Authorization: Bearer $BEARER_TOKEN"
```

```python theme={null}
import requests

response = requests.get(
    "https://api.x.com/2/users/by/username/elonmusk",
    params={"user.fields": "description,public_metrics,profile_image_url,verified,created_at"},
    headers={"Authorization": f"Bearer {BEARER_TOKEN}"},
)
user = response.json()["data"]
followers = user["public_metrics"]["followers_count"]
name = user["name"]
```

```javascript theme={null}
const url = "https://api.x.com/2/users/by/username/elonmusk" +
  "?user.fields=description,public_metrics,profile_image_url,verified,created_at";
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
});
const { data: user } = await res.json();
const followers = user.public_metrics.followers_count;
const name = user.name;
```

**Después: Sorsa API**

```bash theme={null}
curl "https://api.sorsa.io/v3/info?username=elonmusk" \
  -H "ApiKey: $API_KEY"
```

```python theme={null}
import requests

response = requests.get(
    "https://api.sorsa.io/v3/info",
    params={"username": "elonmusk"},
    headers={"ApiKey": API_KEY},
)
user = response.json()
followers = user["followers_count"]
name = user["display_name"]
```

```javascript theme={null}
const res = await fetch("https://api.sorsa.io/v3/info?username=elonmusk", {
  headers: { ApiKey: API_KEY },
});
const user = await res.json();
const followers = user.followers_count;
const name = user.display_name;
```

### Buscar publicaciones

**Antes:**

```bash theme={null}
curl "https://api.x.com/2/tweets/search/recent?query=from%3Aelonmusk%20since%3A2024-01-01&tweet.fields=created_at,public_metrics,lang&expansions=author_id&user.fields=username,name&max_results=10" \
  -H "Authorization: Bearer $BEARER_TOKEN"
```

```python theme={null}
params = {
    "query": "from:elonmusk since:2024-01-01",
    "tweet.fields": "created_at,public_metrics,lang",
    "expansions": "author_id",
    "user.fields": "username,name",
    "max_results": 10,
}
response = requests.get(
    "https://api.x.com/2/tweets/search/recent",
    headers={"Authorization": f"Bearer {BEARER_TOKEN}"},
    params=params,
)
data = response.json()

tweets = data["data"]
users = {u["id"]: u for u in data.get("includes", {}).get("users", [])}
next_token = data.get("meta", {}).get("next_token")

for tweet in tweets:
    author = users.get(tweet["author_id"])
    print(tweet["text"], "by", author["username"])
```

```javascript theme={null}
const params = new URLSearchParams({
  query: "from:elonmusk since:2024-01-01",
  "tweet.fields": "created_at,public_metrics,lang",
  expansions: "author_id",
  "user.fields": "username,name",
  max_results: "10",
});
const res = await fetch(`https://api.x.com/2/tweets/search/recent?${params}`, {
  headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
});
const data = await res.json();

const tweets = data.data || [];
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u]));

for (const t of tweets) {
  const author = users[t.author_id];
  console.log(t.text, "by", author.username);
}
```

**Después:**

```bash theme={null}
curl -X POST "https://api.sorsa.io/v3/search-tweets" \
  -H "ApiKey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "from:elonmusk since:2024-01-01"}'
```

```python theme={null}
response = requests.post(
    "https://api.sorsa.io/v3/search-tweets",
    headers={"ApiKey": API_KEY},
    json={"query": "from:elonmusk since:2024-01-01"},
)
data = response.json()

for tweet in data["tweets"]:
    print(tweet["full_text"], "by", tweet["user"]["username"])
```

```javascript theme={null}
const res = await fetch("https://api.sorsa.io/v3/search-tweets", {
  method: "POST",
  headers: { ApiKey: API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ query: "from:elonmusk since:2024-01-01" }),
});
const data = await res.json();

for (const t of data.tweets) {
  console.log(t.full_text, "by", t.user.username);
}
```

### Recorrer todos los seguidores

**Antes:**

```python theme={null}
def fetch_all_followers_official(user_id, token):
    url = f"https://api.x.com/2/users/{user_id}/followers"
    headers = {"Authorization": f"Bearer {token}"}
    followers = []
    pagination_token = None

    while True:
        params = {"max_results": 1000}
        if pagination_token:
            params["pagination_token"] = pagination_token

        r = requests.get(url, headers=headers, params=params)
        r.raise_for_status()
        data = r.json()

        followers.extend(data.get("data", []))
        pagination_token = data.get("meta", {}).get("next_token")
        if not pagination_token:
            break

    return followers
```

**Después:**

```bash theme={null}
CURSOR=""
while :; do
  RES=$(curl -s "https://api.sorsa.io/v3/followers?username=elonmusk${CURSOR:+&next_cursor=$CURSOR}" \
    -H "ApiKey: $API_KEY")
  echo "$RES" | jq '.users'
  CURSOR=$(echo "$RES" | jq -r '.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done
```

```python theme={null}
def fetch_all_followers(user_id, api_key):
    url = "https://api.sorsa.io/v3/followers"
    headers = {"ApiKey": api_key}
    followers = []
    next_cursor = None

    while True:
        params = {"user_id": user_id}
        if next_cursor:
            params["next_cursor"] = next_cursor

        r = requests.get(url, headers=headers, params=params)
        r.raise_for_status()
        data = r.json()

        followers.extend(data.get("users", []))
        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break

    return followers
```

```javascript theme={null}
async function fetchAllFollowers(userId, apiKey) {
  const followers = [];
  let nextCursor = null;

  do {
    const params = new URLSearchParams({ user_id: userId });
    if (nextCursor) params.set("next_cursor", nextCursor);

    const res = await fetch(
      `https://api.sorsa.io/v3/followers?${params}`,
      { headers: { ApiKey: apiKey } }
    );
    const json = await res.json();

    followers.push(...(json.users || []));
    nextCursor = json.next_cursor || null;
  } while (nextCursor);

  return followers;
}
```

Cada página de Sorsa devuelve hasta 200 perfiles completos. La API oficial suele devolver IDs y datos mínimos, que requieren consultas adicionales para completar perfiles.

## Sintaxis de búsqueda

Sorsa utiliza los operadores de la búsqueda avanzada web de X, que difieren de los de API v2. Conserva palabras, frases, `from:` y `to:` cuando corresponda, adapta los filtros específicos y prueba la consulta. Por ejemplo, usa `-filter:nativeretweets` para excluir retuits nativos.

| Operador            | Ejemplo                                         |
| ------------------- | ----------------------------------------------- |
| `from:`             | `from:elonmusk`                                 |
| `to:`               | `to:elonmusk`                                   |
| `since:` / `until:` | `since:2024-01-01 until:2024-02-01`             |
| Hashtag             | `#bitcoin`                                      |
| Frase exacta        | `"hello world"`                                 |
| `OR`                | `bitcoin OR ethereum`                           |
| Exclusión           | `-filter:nativeretweets`                        |
| Combinación         | `from:elonmusk #bitcoin -filter:nativeretweets` |

Referencia completa: [Operadores de búsqueda](https://docs.sorsa.io/es/search-operators).

`/mentions` admite además `min_likes`, `min_replies`, `min_retweets`, `since_date` y `until_date`. Consulta [Seguimiento de menciones](https://docs.sorsa.io/es/search-mentions).

## Gestión de errores

La API oficial devuelve un array `errors`:

```json theme={null}
{
  "errors": [
    {
      "message": "Not Found",
      "type": "https://api.x.com/2/problems/resource-not-found",
      "title": "Not Found Error",
      "detail": "Could not find tweet with id: [123].",
      "status": 404
    }
  ]
}
```

Sorsa utiliza una estructura sencilla:

```json theme={null}
{ "message": "Tweet not found" }
```

Los códigos comunes son `400`, `401`, `403`, `404`, `429` y `500`. Consulta [Códigos de error](https://docs.sorsa.io/es/error-codes).

Ante un 429, espera y reintenta. El límite común es **20 solicitudes por segundo**, sin ventanas independientes por endpoint. Consulta [Límites de solicitudes](https://docs.sorsa.io/es/rate-limits).

Función de reintento compatible con ambas APIs:

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

def call_with_retry(method, url, max_retries=3, **kwargs):
    for attempt in range(max_retries):
        r = requests.request(method, url, **kwargs)
        if r.status_code == 429:
            time.sleep(2 ** attempt)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"Failed after {max_retries} retries")
```

## Lista de comprobación de la migración

* Sustituye `Authorization: Bearer ...` por `ApiKey: ...`.
* Elimina la firma OAuth 1.0a de las lecturas migradas: claves de consumidor, tokens y generación de firmas.
* Cambia `https://api.x.com/2` por `https://api.sorsa.io/v3`.
* Adapta cada ruta según las tablas.
* Cambia GET por POST en publicaciones, búsquedas, comentarios, citas y usuarios que retuitearon.
* Elimina `tweet.fields`, `user.fields`, `media.fields` y `expansions`.
* Actualiza los lectores de respuestas: elimina la extracción de `data`, `includes` y `meta`.
* Cambia los nombres de campos, como `name` por `display_name` y `text` por `full_text`.
* Accede a las métricas sin `public_metrics`.
* Sustituye `pagination_token` y `next_token` por `next_cursor`.
* Adapta los errores a `{ "message": "..." }`.
* Aplica el límite común de 20 solicitudes por segundo.
* Prueba los endpoints críticos en [API Playground](https://api.sorsa.io/playground).
* Supervisa la cuota con `GET /key-usage-info`.
* Conserva la API oficial para operaciones de escritura si las necesitas.

## Funciones sin equivalente en la API oficial

| Función                                                  | Endpoint                                            |
| -------------------------------------------------------- | --------------------------------------------------- |
| Cronología sin límite de 3.200 publicaciones             | `POST /user-tweets`                                 |
| Verificación de seguimiento en una llamada               | `POST /check-follow`                                |
| Verificación de retuit en una llamada                    | `POST /check-retweet`                               |
| Verificación de comentario en una llamada                | `GET /check-comment`                                |
| Verificación de cita o retuit en una llamada             | `POST /check-quoted`                                |
| Verificación de pertenencia a comunidad                  | `POST /check-community-member`                      |
| Miembros y publicaciones de comunidades                  | `POST /community-members`, `POST /community-tweets` |
| Búsqueda dentro de comunidades                           | `POST /community-search-tweets`                     |
| Contenido de artículos largos de X                       | `POST /article`                                     |
| Filtro de seguidores verificados                         | `GET /verified-followers`                           |
| País e historial de cambios de nombre                    | `GET /about`                                        |
| Puntuación de influencia                                 | `GET /score`, `GET /score-changes`                  |
| Principales seguidores y cuentas seguidas por influencia | `GET /top-followers`, `GET /top-following`          |
| Desglose de seguidores por categoría                     | `GET /followers-stats`                              |

## Referencias relacionadas

* [Autenticación](https://docs.sorsa.io/es/authentication)
* [URL base y versiones](https://docs.sorsa.io/es/base-url-and-versioning)
* [Límites de solicitudes](https://docs.sorsa.io/es/rate-limits)
* [Paginación](https://docs.sorsa.io/es/pagination)
* [Códigos de error](https://docs.sorsa.io/es/error-codes)
* [Formato de respuesta](https://docs.sorsa.io/es/response-format)
* [Operadores de búsqueda](https://docs.sorsa.io/es/search-operators)
* [Optimización del uso de la API](https://docs.sorsa.io/es/optimizing-api-usage)
* [Referencia de la API](https://docs.sorsa.io/es/api-reference-guide)
