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

# Verificación de campañas de marketing

# Verifica acciones en X mediante API: seguimientos, retuits, comentarios y citas

Las campañas con recompensas piden seguir una cuenta, retuitear, comentar o unirse a una comunidad. Para repartir premios de forma justa, hay que comprobar que cada participante realizó las acciones. La revisión manual deja de ser práctica con muchos usuarios y las casillas de autoconfirmación facilitan el fraude.

Los endpoints de verificación de Sorsa responden a preguntas concretas sobre esas acciones con un resultado sí/no o un estado. Permiten crear sistemas de tareas, sorteos, programas de recomendación y campañas basados en datos verificables y auditables.

Esta guía presenta los endpoints con código y los combina en un flujo completo. Las 100 solicitudes gratuitas de cada cuenta, sin tarjeta, permiten probarlo antes de elegir un plan.

> **Nota:** consulta más flujos y ejemplos en la [guía completa de verificación de campañas](https://api.sorsa.io/blog/twitter-engagement-verification-api).

***

## Comprobaciones disponibles

| Acción                               | Endpoint                  | Método | Respuesta                                        |
| :----------------------------------- | :------------------------ | :----- | :----------------------------------------------- |
| El usuario sigue una cuenta          | `/check-follow`           | POST   | `{"follow": true/false}`                         |
| El usuario retuiteó                  | `/check-retweet`          | POST   | `{"retweet": true/false}`                        |
| El usuario citó                      | `/check-quoted`           | POST   | `{"status": "quoted" / "retweet" / "not_found"}` |
| El usuario comentó                   | `/check-comment`          | GET    | `{"commented": true/false}`                      |
| El usuario pertenece a una comunidad | `/check-community-member` | POST   | `{"is_member": true/false}`                      |

**Lo que no puedes verificar: los Me gusta.** X los hizo privados en 2024. Ninguna API, incluida la oficial, puede comprobar si un usuario concreto marcó una publicación con Me gusta. Diseña tus campañas con las cinco acciones anteriores.

***

## Comprobación 1: ¿El usuario sigue una cuenta?

La tarea más habitual: «Sigue a @YourBrand para participar».

**Endpoint:** `POST /v3/check-follow`

Responde a «¿user\_2 sigue a user\_1?». `user_1` es la marca o cuenta seguida; `user_2` es el participante.

### Ejemplo básico

```bash theme={null}
curl -X POST https://api.sorsa.io/v3/check-follow \
  -H "ApiKey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "username_1": "YourBrand",
    "username_2": "participant_handle"
  }'
```

Respuesta:

```json theme={null}
{
  "follow": true,
  "user_protected": false
}
```

### Parámetros

Proporciona exactamente un identificador por cada lado:

| Cuenta                             | Opciones: proporciona una                 |
| :--------------------------------- | :---------------------------------------- |
| Marca: `user_1`, la cuenta seguida | `username_1`, `user_link_1` o `user_id_1` |
| Participante: `user_2`             | `username_2`, `user_link_2` o `user_id_2` |

### Python

```python theme={null}
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY, "Content-Type": "application/json"}

def check_follow(brand_handle: str, participant_handle: str) -> dict:
    resp = requests.post(
        f"{BASE}/check-follow",
        headers=HEADERS,
        json={"username_1": brand_handle, "username_2": participant_handle},
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json()


result = check_follow("YourBrand", "participant123")
if result["follow"]:
    print("Follow verified.")
elif result.get("user_protected"):
    print("Account is private; follow cannot be confirmed.")
else:
    print("Not following.")
```

Si `user_protected` es `true`, la cuenta del participante es privada y no se pueden verificar sus relaciones de seguimiento.

***

## Comprobación 2: ¿El usuario retuiteó?

«Retuitea esta publicación para participar». El endpoint examina hasta 100 retuits por solicitud y admite paginación.

**Endpoint:** `POST /v3/check-retweet`

### Parámetros

| Parámetro                            | Tipo   | Obligatorio | Descripción                                       |
| :----------------------------------- | :----- | :---------- | :------------------------------------------------ |
| `tweet_link`                         | string | Sí          | URL o ID de la publicación.                       |
| `username` / `user_link` / `user_id` | string | Sí, uno     | Participante. Proporciona exactamente uno.        |
| `next_cursor`                        | string | No          | Cursor para publicaciones con más de 100 retuits. |

### Python

```python theme={null}
def check_retweet(tweet_link: str, participant_handle: str) -> bool:
    cursor = None
    for _ in range(5):  # check up to 500 retweets total
        body = {"tweet_link": tweet_link, "username": participant_handle}
        if cursor:
            body["next_cursor"] = cursor
        resp = requests.post(f"{BASE}/check-retweet", headers=HEADERS, json=body, timeout=15)
        resp.raise_for_status()
        data = resp.json()
        if data["retweet"]:
            return True
        cursor = data.get("next_cursor")
        if not cursor:
            return False
    raise RuntimeError("Retweet verification incomplete: page limit reached")
```

Cada llamada examina un lote de hasta 100 retuits, empezando por los más recientes. En muchas campañas basta una solicitud porque la acción se realiza poco después del inicio. Para publicaciones populares donde el participante retuiteó antes, recorre `next_cursor`.

***

## Comprobación 3: ¿El usuario citó?

«Cita esta publicación y añade tu opinión». `/check-quoted` distingue una cita de un retuit sin comentario y devuelve un estado.

**Endpoint:** `POST /v3/check-quoted`

### Python

```python theme={null}
def check_quoted(tweet_link: str, participant_handle: str) -> dict:
    resp = requests.post(
        f"{BASE}/check-quoted",
        headers=HEADERS,
        json={"tweet_link": tweet_link, "username": participant_handle},
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json()


data = check_quoted("https://x.com/YourBrand/status/1234567890", "participant123")

if data["status"] == "quoted":
    print(f"Quote verified! They wrote: {data['text']}")
elif data["status"] == "retweet":
    print("They retweeted but did not quote.")
else:
    print("No quote or retweet found.")
```

### Respuesta

```json theme={null}
{
  "status": "quoted",
  "date": "2026-03-10 14:22:09",
  "text": "This is amazing, everyone should check this out!",
  "user_protected": false
}
```

`status` puede ser `"quoted"` (cita), `"retweet"` (retuit sin texto) o `"not_found"` (ninguna acción detectada). Si existe una cita, incluye fecha y texto para comprobar longitud mínima, hashtags obligatorios o lenguaje inapropiado.

```python theme={null}
def quote_is_acceptable(quote_text: str, min_length: int = 30, required_hashtag: str = None) -> bool:
    if len(quote_text.strip()) < min_length:
        return False
    if required_hashtag and required_hashtag.lower() not in quote_text.lower():
        return False
    return True
```

***

## Comprobación 4: ¿El usuario comentó?

«Deja un comentario en esta publicación». Es el único de estos endpoints que utiliza GET.

**Endpoint:** `GET /v3/check-comment`

### Parámetros de consulta

| Parámetro                            | Tipo   | Obligatorio | Descripción                                |
| :----------------------------------- | :----- | :---------- | :----------------------------------------- |
| `tweet_link`                         | string | Sí          | URL o ID de la publicación.                |
| `username` / `user_link` / `user_id` | string | Sí, uno     | Participante. Proporciona exactamente uno. |

### Python

```python theme={null}
def check_comment(tweet_link: str, participant_handle: str) -> dict:
    resp = requests.get(
        f"{BASE}/check-comment",
        headers={"ApiKey": API_KEY},
        params={"tweet_link": tweet_link, "username": participant_handle},
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json()


data = check_comment("https://x.com/YourBrand/status/1234567890", "participant123")

if data["commented"]:
    print(f"Comment verified: {data['tweet']['full_text'][:100]}")
else:
    print("No comment found.")
```

Si `commented` es `true`, la respuesta incluye el objeto `tweet` completo del comentario: texto, métricas y fecha. Puedes exigir longitud mínima, un hashtag o algo más que emojis.

```python theme={null}
def comment_is_acceptable(comment: dict, min_length: int = 20, required_keyword: str = None) -> bool:
    text = comment.get("full_text", "").strip()
    if len(text) < min_length:
        return False
    if required_keyword and required_keyword.lower() not in text.lower():
        return False
    if len(text.split()) < 3:
        return False
    return True
```

***

## Comprobación 5: ¿El usuario pertenece a una comunidad?

Confirma la disponibilidad actual de datos de comunidades con [soporte](https://docs.sorsa.io/es/support) antes de exigir esta tarea. Consulta la nota de disponibilidad en [Listas y comunidades](https://docs.sorsa.io/es/lists-and-communities).

«Únete a nuestra comunidad de X para participar». Es útil cuando la pertenencia es un requisito.

**Endpoint:** `POST /v3/check-community-member`

### Python

```python theme={null}
def check_community_member(community_id: str, participant_handle: str) -> bool:
    resp = requests.post(
        f"{BASE}/check-community-member",
        headers=HEADERS,
        json={"community_id": community_id, "username": participant_handle},
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json().get("is_member", False)


is_member = check_community_member("1966045657589813686", "participant123")
print("Member" if is_member else "Not a member")
```

El ID de la comunidad es la cadena numérica de `x.com/i/communities/<id>`.

***

## Crear un flujo de verificación de campañas

El siguiente patrón ejecuta las cinco comprobaciones para un participante, devuelve resultados estructurados y aplica criterios de calidad al comentario y a la cita.

```python theme={null}
from dataclasses import dataclass, field

@dataclass
class CampaignConfig:
    brand_handle: str
    tweet_to_retweet: str
    tweet_to_quote: str
    tweet_to_comment: str
    community_id: str
    required_hashtag: str = ""
    min_quote_length: int = 30
    min_comment_length: int = 20

@dataclass
class ParticipantResult:
    username: str
    follow: bool = False
    retweet: bool = False
    quote: bool = False
    quote_text: str = ""
    comment: bool = False
    comment_text: str = ""
    community: bool = False
    completed: int = field(init=False, default=0)

    def total(self) -> int:
        return sum([self.follow, self.retweet, self.quote, self.comment, self.community])


def verify_participant(username: str, cfg: CampaignConfig) -> ParticipantResult:
    r = ParticipantResult(username=username)

    r.follow = check_follow(cfg.brand_handle, username)["follow"]
    r.retweet = check_retweet(cfg.tweet_to_retweet, username)

    quote_data = check_quoted(cfg.tweet_to_quote, username)
    if quote_data["status"] == "quoted":
        r.quote_text = quote_data.get("text", "")
        r.quote = quote_is_acceptable(r.quote_text, cfg.min_quote_length, cfg.required_hashtag)

    comment_data = check_comment(cfg.tweet_to_comment, username)
    if comment_data.get("commented"):
        r.comment_text = comment_data["tweet"].get("full_text", "")
        r.comment = comment_is_acceptable(comment_data["tweet"], cfg.min_comment_length)

    r.community = check_community_member(cfg.community_id, username)

    r.completed = r.total()
    return r


cfg = CampaignConfig(
    brand_handle="YourBrand",
    tweet_to_retweet="https://x.com/YourBrand/status/111111111",
    tweet_to_quote="https://x.com/YourBrand/status/222222222",
    tweet_to_comment="https://x.com/YourBrand/status/333333333",
    community_id="1966045657589813686",
    required_hashtag="#YourLaunch",
)

result = verify_participant("participant123", cfg)
print(f"@{result.username}: {result.completed}/5 tasks done")
```

La primera página de cada comprobación consume cinco solicitudes en total. La paginación de retuits y los reintentos añaden llamadas: cinco es una base, no un coste fijo. Agotar el presupuesto de páginas significa que la verificación está incompleta, no que el participante incumplió la tarea.

***

## Verificar participantes en lote

Este patrón regula las solicitudes, guarda resultados en CSV y permite reanudar el trabajo. Escribe una fila después de cada participante para conservar el progreso ante una caída.

```python theme={null}
import csv
import time
from pathlib import Path

def verify_campaign_batch(usernames: list[str], cfg: CampaignConfig, output_file: str) -> None:
    fields = ["username", "follow", "retweet", "quote", "comment", "community",
              "completed", "quote_text", "comment_text"]

    already_done = set()
    out_path = Path(output_file)
    if out_path.exists():
        with out_path.open() as f:
            already_done = {row["username"] for row in csv.DictReader(f)}

    mode = "a" if out_path.exists() else "w"
    with out_path.open(mode, newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fields)
        if mode == "w":
            writer.writeheader()

        for i, username in enumerate(usernames):
            if username in already_done:
                continue

            for attempt in range(3):
                try:
                    r = verify_participant(username, cfg)
                    writer.writerow({
                        "username": r.username,
                        "follow": r.follow,
                        "retweet": r.retweet,
                        "quote": r.quote,
                        "comment": r.comment,
                        "community": r.community,
                        "completed": r.completed,
                        "quote_text": r.quote_text,
                        "comment_text": r.comment_text,
                    })
                    f.flush()
                    already_done.add(username)
                    print(f"[{i+1}/{len(usernames)}] @{username}: {r.completed}/5")
                    break
                except RuntimeError as e:
                    print(f"[{i+1}] @{username}: INCOMPLETE {e}")
                    break
                except requests.HTTPError as e:
                    if e.response.status_code == 429:
                        time.sleep(5)
                        continue  # retry the same participant
                    print(f"[{i+1}] @{username}: ERROR {e}")
                    break

            time.sleep(0.25)


participants = open("entries.txt").read().splitlines()
verify_campaign_batch(participants, cfg, "campaign_results.csv")
```

El bucle espera entre participantes y reintenta un 429 hasta tres veces, pero la paginación puede añadir llamadas dentro de cada participante. Utiliza un limitador compartido en grupos de procesos de producción. La velocidad real depende de la profundidad de páginas y de la latencia. Los casos incompletos o fallidos deben quedar pendientes de reintento, sin registrarse como resultados negativos.

***

## Verificar la titularidad de una cuenta

Antes de aceptar al participante, puedes confirmar que controla el nombre de X indicado:

1. Genera un código único, como `VERIFY-a8f3b2`, y muéstraselo.
2. Pídele que publique ese código.
3. Consulta sus publicaciones recientes con `/user-tweets` y búscalo.

```python theme={null}
import secrets

def generate_verification_code() -> str:
    return f"VERIFY-{secrets.token_hex(4)}"


def verify_account_ownership(username: str, expected_code: str) -> bool:
    resp = requests.post(
        f"{BASE}/user-tweets",
        headers=HEADERS,
        json={"username": username},
        timeout=15,
    )
    resp.raise_for_status()
    tweets = resp.json().get("tweets", [])

    for tweet in tweets:
        author = tweet.get("user") or {}
        if (author.get("username", "").lower() == username.lstrip("@").lower()
                and not tweet.get("retweeted_status")
                and expected_code in (tweet.get("full_text") or "")):
            return True
    return False


code = generate_verification_code()
print(f"Ask the user to tweet: {code}")
# ... after the user tweets ...
if verify_account_ownership("participant123", code):
    print("Account ownership confirmed.")
```

Vincula cada desafío al participante autenticado y a la cuenta de X, establece una caducidad corta y úsalo una sola vez. Comprueba autor y fecha contra el desafío. El ejemplo comprueba autor y texto; tu aplicación debe implementar almacenamiento, caducidad y uso único. El participante puede eliminar la publicación tras la verificación.

***

## Criterios contra el fraude

Utiliza estas comprobaciones como criterios configurables de elegibilidad o revisión. La antigüedad y los contadores no demuestran si una cuenta es legítima:

* **Antigüedad mínima.** Consulta `/info` y `created_at`. Puedes rechazar cuentas de menos de 30 días; muchas redes de bots utilizan cuentas nuevas.
* **Actividad mínima.** Revisa `tweets_count` y `followers_count`. Valores bajos pueden justificar revisión, pero no prueban que sea un bot.
* **Calidad de comentarios.** Usa el texto de `/check-comment` para exigir longitud, palabras clave o hashtags y rechazar respuestas de un carácter o solo emojis.
* **Calidad de citas.** Aplica criterios similares al texto de `/check-quoted`.
* **Velocidad de finalización.** Completar tareas muy rápido es una señal para revisar, no una prueba de automatización. Registra las horas y marca los casos sospechosos.

```python theme={null}
from datetime import datetime, timezone

def is_legitimate_account(
    username: str,
    min_age_days: int = 30,
    min_tweets: int = 10,
    min_followers: int = 5,
) -> tuple[bool, dict]:
    resp = requests.get(
        f"{BASE}/info",
        headers={"ApiKey": API_KEY},
        params={"username": username},
        timeout=15,
    )
    resp.raise_for_status()
    profile = resp.json()

    created = datetime.fromisoformat(profile["created_at"].replace("Z", "+00:00"))
    age_days = (datetime.now(timezone.utc) - created).days

    checks = {
        "account_age_ok": age_days >= min_age_days,
        "has_tweets": profile.get("tweets_count", 0) >= min_tweets,
        "has_followers": profile.get("followers_count", 0) >= min_followers,
        "not_protected": not profile.get("protected", False),
    }
    return all(checks.values()), checks
```

Ejecuta este filtro antes de las cinco verificaciones. Si `is_legitimate_account` devuelve `False`, evitas cinco llamadas para un participante que no cumpliría los criterios.

***

## Ponderar participantes por influencia

Las audiencias no son iguales. Una cuenta con 50.000 seguidores puede aportar más valor a una campaña que una con 50. Consulta `/info` y ajusta la recompensa según los seguidores.

```python theme={null}
import math

BASE_POINTS = {"follow": 10, "retweet": 15, "quote": 25, "comment": 20, "community": 10}

def get_follower_count(username: str) -> int:
    resp = requests.get(
        f"{BASE}/info",
        headers={"ApiKey": API_KEY},
        params={"username": username},
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json().get("followers_count", 0)


def calculate_weighted_points(result: ParticipantResult) -> dict:
    followers = get_follower_count(result.username)
    # log scaling: 100 followers -> 2x, 10K -> 4x, 1M -> 6x
    multiplier = max(1.0, math.log10(followers + 1))
    total = 0
    breakdown = {}
    for task, base in BASE_POINTS.items():
        if getattr(result, task):
            points = round(base * multiplier)
            breakdown[task] = points
            total += points
    return {"followers": followers, "multiplier": round(multiplier, 2),
            "breakdown": breakdown, "total": total}
```

En campañas de criptomonedas, puedes sustituir este multiplicador por [Sorsa Score](https://docs.sorsa.io/es/sorsa-score-and-crypto-analytics), que mide reconocimiento entre líderes de opinión, proyectos y fondos del sector.

***

## Nota sobre los Me gusta

X hizo privados los Me gusta en 2024. Ninguna API pública de Sorsa, X u otro proveedor expone qué usuarios marcaron una publicación con Me gusta. Sustituye esa tarea por retuitear o comentar, acciones que sí se pueden verificar.

***

## Próximos pasos

* [Búsqueda de publicaciones](https://docs.sorsa.io/es/search-tweets): supervisión de campañas por palabras clave.
* [Seguimiento de menciones](https://docs.sorsa.io/es/search-mentions): menciones orgánicas y de campaña.
* [Supervisión en tiempo real](https://docs.sorsa.io/es/real-time-monitoring): verifica nueva actividad con consultas periódicas.
* [Seguidores y cuentas seguidas](https://docs.sorsa.io/es/followers-and-following): cruza seguidores y participantes.
* [Precios](https://api.sorsa.io/pricing): estima el coste de la campaña, con cinco solicitudes iniciales por participante.
* [Referencia de la API](https://docs.sorsa.io/es/api-reference-guide): especificación de verificación.
