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

# Verificação de campanhas de marketing

# Verifique ações no X: seguir, repostar, comentar e citar

Campanhas com recompensas pedem ações como seguir uma conta, repostar, comentar ou entrar em uma comunidade. Para distribuir recompensas, confirme que os participantes concluíram as tarefas. Verificação manual não escala, e caixas de seleção baseadas apenas na declaração do usuário facilitam fraudes.

A Sorsa oferece endpoints específicos com resultados booleanos ou status, permitindo criar missões, sorteios, programas de indicação e campanhas auditáveis. Este guia reúne exemplos e um pipeline completo. As 100 requisições gratuitas de cada conta, sem cartão, permitem prototipar o fluxo.

> Veja mais exemplos no [guia completo de campanhas](https://api.sorsa.io/blog/twitter-engagement-verification-api).

## Verificações disponíveis

| Ação                 | Endpoint                  | Método | Retorno                                          |
| :------------------- | :------------------------ | :----- | :----------------------------------------------- |
| Segue uma conta      | `/check-follow`           | POST   | `{"follow": true/false}`                         |
| Repostou             | `/check-retweet`          | POST   | `{"retweet": true/false}`                        |
| Citou                | `/check-quoted`           | POST   | `{"status": "quoted" / "retweet" / "not_found"}` |
| Comentou             | `/check-comment`          | GET    | `{"commented": true/false}`                      |
| Entrou na comunidade | `/check-community-member` | POST   | `{"is_member": true/false}`                      |

**Curtidas não podem ser verificadas.** O X as tornou privadas em 2024. Nem a API oficial permite verificar se uma pessoa curtiu um post específico. Planeje campanhas com as cinco ações acima.

## 1. O usuário seguiu a conta?

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

A pergunta é: “user\_2 segue user\_1?”. Use a marca como `user_1` e o participante como `user_2`.

### Exemplo

```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"
  }'
```

Resposta:

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

Envie exatamente um identificador de cada lado:

| Lado                            | Opções                                     |
| :------------------------------ | :----------------------------------------- |
| Marca (`user_1`, conta seguida) | `username_1`, `user_link_1` ou `user_id_1` |
| Participante (`user_2`)         | `username_2`, `user_link_2` ou `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.")
```

Se `user_protected` for `true`, a conta é privada e as relações não podem ser verificadas.

## 2. O usuário repostou?

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

Examina até 100 repostagens por chamada e permite paginação.

| Parâmetro                            | Tipo   | Obrigatório | Descrição                                 |
| :----------------------------------- | :----- | :---------- | :---------------------------------------- |
| `tweet_link`                         | string | Sim         | URL ou ID da publicação.                  |
| `username` / `user_link` / `user_id` | string | Um          | Identificador do participante.            |
| `next_cursor`                        | string | Não         | Cursor quando há mais de 100 repostagens. |

```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")
```

A primeira chamada examina as 100 mais recentes. Participações recentes podem aparecer nela; para posts populares e ações antigas, percorra `next_cursor`.

## 3. O usuário citou?

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

Distingue citação com comentário de repostagem simples.

```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.")
```

### Resposta

```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` pode ser `"quoted"`, `"retweet"` ou `"not_found"`. Citações incluem data e texto para verificar tamanho mínimo, hashtag obrigatória ou linguagem inadequada.

```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
```

## 4. O usuário comentou?

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

É a única das cinco verificações que usa GET.

| Parâmetro                            | Tipo   | Obrigatório | Descrição                      |
| :----------------------------------- | :----- | :---------- | :----------------------------- |
| `tweet_link`                         | string | Sim         | URL ou ID da publicação.       |
| `username` / `user_link` / `user_id` | string | Um          | Identificador do participante. |

```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.")
```

Quando `commented` é verdadeiro, a resposta inclui o objeto `tweet` completo do comentário, com texto, métricas e data. Use-o para verificar tamanho, hashtags ou respostas compostas apenas por 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
```

## 5. O usuário faz parte da comunidade?

Confirme a disponibilidade atual dos dados com o [suporte](https://docs.sorsa.io/pt-BR/support) antes de exigir essa tarefa. Veja a nota em [listas e comunidades](https://docs.sorsa.io/pt-BR/lists-and-communities).

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

```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")
```

O ID da comunidade é a string numérica em `x.com/i/communities/<id>`.

## Pipeline de verificação

Este exemplo executa as cinco verificações e aplica regras de qualidade a comentários e citações.

```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")
```

A primeira página de cada verificação custa cinco requisições. Paginação de repostagens e novas tentativas aumentam o total. Atingir um orçamento de páginas significa **verificação incompleta**, não tarefa reprovada.

## Verificar participantes em lote

O exemplo respeita pausas, grava CSV após cada participante e permite retomada.

```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")
```

O loop espera entre participantes e repete um 429 até três vezes, mas a paginação pode gerar chamadas adicionais dentro de cada participante. Em produção com vários workers, use um limitador compartilhado. O rendimento depende de latência e profundidade. Falhas e resultados incompletos devem continuar elegíveis para nova tentativa, sem serem registrados como negativos.

## Verificação de propriedade da conta

1. Gere um código único, como `VERIFY-a8f3b2`.
2. Peça ao usuário que publique o código.
3. Consulte `/user-tweets` e procure a publicação.

```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.")
```

Vincule o desafio ao participante autenticado e à conta pretendida, com validade curta e uso único. Verifique autor e horário da publicação. O exemplo confere autor e texto; sua aplicação deve implementar armazenamento, expiração e consumo único. O participante pode excluir o post depois.

## Critérios antifraude

Use critérios configuráveis de elegibilidade ou revisão. Idade e contadores não provam legitimidade:

* **Idade mínima:** consulte `/info` e `created_at`. Uma regra de campanha pode rejeitar contas com menos de 30 dias.
* **Atividade mínima:** `tweets_count` e `followers_count` baixos podem motivar revisão, mas não provam que a conta é um bot.
* **Qualidade de comentários:** verifique tamanho, palavras-chave e hashtags; rejeite respostas de um caractere ou só emojis conforme suas regras.
* **Qualidade de citações:** aplique critérios semelhantes ao texto retornado.
* **Velocidade de conclusão:** registre horários; conclusão muito rápida é sinal de revisão, não prova de automação.

```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
```

Execute antes das cinco verificações. Se `is_legitimate_account` retornar `False`, você evita chamadas para um participante que já não atende aos critérios.

## Pontuar participantes por influência

Você pode ponderar recompensas pelo tamanho da audiência, consultando o perfil com `/info`.

```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}
```

Em campanhas cripto, considere o [Sorsa Score](https://docs.sorsa.io/pt-BR/sorsa-score-and-crypto-analytics), que mede reconhecimento entre influenciadores, projetos e fundos.

## Sobre curtidas

Desde 2024, o X não expõe publicamente quem curtiu uma publicação. Substitua tarefas de curtida por repostagem ou comentário verificável.

## Próximos passos

* [Busca](https://docs.sorsa.io/pt-BR/search-tweets): monitoramento por palavras-chave.
* [Menções](https://docs.sorsa.io/pt-BR/search-mentions): menções orgânicas e de campanha.
* [Monitoramento](https://docs.sorsa.io/pt-BR/real-time-monitoring): consultas periódicas de atividade.
* [Seguidores](https://docs.sorsa.io/pt-BR/followers-and-following): compare com participantes.
* [Preços](https://api.sorsa.io/pricing): cinco chamadas iniciais por participante, mais páginas e tentativas.
* [Referência da API](https://docs.sorsa.io/pt-BR/api-reference-guide): todos os endpoints.
