🏢 Developer Portal X.com
Toda integração com o X começa no developer.x.com. Você precisa de uma conta X normal (com telefone verificado) e um cadastro de developer separado — mesma identidade, contexto diferente. O cadastro pede um caso de uso descrito em inglês.
O modelo é em três níveis: Account → Project → App. A Account é você; um Project agrupa apps relacionados (uma marca, um produto); um App contém as credenciais que sua aplicação usa. Para automação simples, basta um Project com um App.
📋 Checklist do cadastro
- Telefone verificado na conta X — sem isso o Portal recusa o acesso.
- Descrição de uso em inglês (300+ caracteres): o que sua app faz e por quê.
- Aceite dos Restricted Use Cases — proibido spam, surveillance, ML scraping massivo.
- Project criado com nome, caso de uso e descrição pública.
# Fluxo no Portal (manual no browser)
# 1. https://developer.x.com → Sign up
# 2. Use case: "Building tools for myself" (mais rápido de aprovar)
# 3. Criar Project: nome, caso, descrição
# 4. Dentro do Project → "+ Add App"
# 5. Anotar App ID e abrir "Keys and tokens"
Conceitos-chave
Cadastro paralelo à conta X normal.
Agrupador de apps por contexto de negócio.
Onde vivem keys, secrets e tokens de acesso.
Texto explicando o uso — base da aprovação.
💵 Custo: pay-per-use $0.01/post
O X reformulou os tiers em 2024-2025. O Free tier deixou de permitir POST /tweets em volume útil: você lê alguns endpoints e posta apenas como teste. Para automação real, há dois caminhos práticos.
O pay-per-use (lançado 2025) cobra $0.01 por post — ideal para baixo volume, sem mensalidade. Já o Basic, a $200/mês, libera 3.000 posts/mês + leitura ampliada. Acima disso: Pro ($5.000/mês).
⚡ Pay-per-use
- $0.01 por
POST /2/tweets - Sem mensalidade — só paga o que usa
- Ideal: até ~5.000 posts/mês
- Mesmo limite de rate da conta
📦 Basic — $200/mês
- 3.000 posts/mês inclusos
- 10.000 leituras/mês
- OAuth 1.0a + OAuth 2.0 disponíveis
- Quebra ponto: ~20.000 posts vs pay-per-use
💡 Dica prática
Comece pay-per-use mesmo se planeja crescer. Migre para Basic só quando seu custo_mensal_pay_per_use > $200 — a matemática é direta: posts_no_mes × 0.01 > 200, ou seja, ~20k posts. Abaixo disso, pay-per-use é estritamente mais barato.
Conceitos-chave
Quase só leitura — inviável para automação.
Cobrança por chamada, sem assinatura.
Tier flat $200 com quota mensal.
Limite por janela de tempo, paralelo ao custo.
⚙️ Criar app + OAuth 2.0
Dentro do App, abra User authentication settings e habilite OAuth 2.0. Para automação no servidor, escolha tipo Confidential client (gera Client Secret) com flow Authorization Code + PKCE.
Configure a Callback URL (mesmo domínio do seu app, ex.: https://app.exemplo.com/oauth/x/callback) e selecione os scopes mínimos necessários — sempre o menor conjunto possível.
// scopes essenciais para postagem
const SCOPES = [
"tweet.read", // ler tweets
"tweet.write", // criar tweets — obrigatório
"users.read", // ler dados do usuário autenticado
"offline.access" // receber refresh_token (renovação automática)
] as const;
// URL de autorização — usuário vê tela de consent do X
const authUrl = new URL("https://x.com/i/oauth2/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", process.env.X_CLIENT_ID!);
authUrl.searchParams.set("redirect_uri", "https://app.exemplo.com/oauth/x/callback");
authUrl.searchParams.set("scope", SCOPES.join(" "));
authUrl.searchParams.set("state", crypto.randomUUID());
authUrl.searchParams.set("code_challenge", codeChallenge); // PKCE
authUrl.searchParams.set("code_challenge_method", "S256");
console.log(authUrl.toString());
✓ O que FAZER
- ✓Usar
Confidential clientem servidor backend. - ✓Pedir
offline.accesspara refresh automático. - ✓Validar o
stateno callback contra CSRF. - ✓Persistir
refresh_tokencriptografado.
✗ O que NÃO fazer
- ✗Embutir
Client Secretno frontend. - ✗Reusar
code_verifierentre requests. - ✗Pedir scopes demais "por garantia" — quebra trust.
- ✗Usar
http://em callback de prod.
Conceitos-chave
Padrão para autorização delegada.
Code challenge que evita interceptação.
Endpoint que recebe o code.
Permissões granulares concedidas pelo usuário.
📦 Lib twitter-api-v2
O cliente oficial-de-fato em Node/TypeScript é twitter-api-v2. Cobre v2 + v1.1 (necessária para upload de mídia), tem tipagem completa, faz refresh automático de token e expõe helpers para threads e paginação.
# Instalação
npm install twitter-api-v2
npm install -D @types/node typescript
// src/x-client.ts
import { TwitterApi } from "twitter-api-v2";
// Cliente OAuth 2.0 (usuário autenticado via Authorization Code)
export function userClient(accessToken: string) {
return new TwitterApi(accessToken);
}
// Cliente App-only (apenas leitura pública, sem usuário)
export function appClient() {
return new TwitterApi(process.env.X_BEARER_TOKEN!);
}
// Cliente OAuth 1.0a (necessário para upload v1.1)
export function userContextClient() {
return new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: process.env.X_ACCESS_TOKEN!,
accessSecret: process.env.X_ACCESS_SECRET!,
});
}
// Refresh automático (offline.access ligado)
export async function refresh(refreshToken: string) {
const base = new TwitterApi({
clientId: process.env.X_CLIENT_ID!,
clientSecret: process.env.X_CLIENT_SECRET!,
});
const { accessToken, refreshToken: newRefresh, expiresIn } =
await base.refreshOAuth2Token(refreshToken);
return { accessToken, refreshToken: newRefresh, expiresIn };
}
⚙️ Dica prática
A lib distingue três contextos de auth (user OAuth2, app-only, user OAuth1.0a) e cada um expõe endpoints diferentes. Memorize: postar texto = OAuth 2.0 user; upload de mídia = OAuth 1.0a (a v2 ainda não cobre); buscar tweets públicos = app-only com Bearer.
Conceitos-chave
Age em nome de um usuário específico.
Bearer estático para leitura pública.
Troca por novo access token sem reconsentir.
Tipos completos para v2 — autocomplete real.
🖼️ Upload de mídia (ainda v1.1)
Em 2026 o upload de imagens/vídeos para o X ainda vive na v1.1 — o endpoint media/upload.json não foi migrado. O fluxo é: subir o arquivo, receber um media_id, e referenciá-lo na criação do tweet via v2.
Por isso o app costuma precisar de dois contextos de auth: OAuth 1.0a para o upload, OAuth 2.0 para o post. A lib twitter-api-v2 esconde essa complexidade — você só precisa instanciar os dois clientes.
// src/post-with-media.ts
import { TwitterApi, EUploadMimeType } from "twitter-api-v2";
import { readFile } from "node:fs/promises";
interface PostWithMediaInput {
text: string;
imagePath: string; // ex.: "./assets/banner.png"
altText?: string; // acessibilidade
}
export async function postWithMedia(
v1Client: TwitterApi, // OAuth 1.0a — para upload
v2Client: TwitterApi, // OAuth 2.0 — para criar tweet
input: PostWithMediaInput
) {
// 1) Upload via v1.1 -> retorna media_id como string
const buffer = await readFile(input.imagePath);
const mediaId = await v1Client.v1.uploadMedia(buffer, {
mimeType: EUploadMimeType.Png,
});
// 2) Anexar alt text (acessibilidade — obrigatório para boas práticas)
if (input.altText) {
await v1Client.v1.createMediaMetadata(mediaId, {
alt_text: { text: input.altText },
});
}
// 3) Criar tweet via v2 referenciando o media_id
const { data } = await v2Client.v2.tweet({
text: input.text,
media: { media_ids: [mediaId] },
});
return { tweetId: data.id, mediaId };
}
📐 Limites de mídia
- Imagem: até 5 MB (PNG, JPG, WebP) — máximo 4 por tweet.
- GIF: até 15 MB — 1 por tweet, exclusivo.
- Vídeo: até 512 MB / 140 s — upload em chunks (init/append/finalize).
- media_id: válido por ~24 h se não consumido.
Conceitos-chave
Endpoints antigos ainda usados para upload.
Referência string para usar no v2.tweet.
Vídeos: init → append → finalize.
Descrição para leitores de tela — sempre setar.
🧵 Threads programáticos
Thread no X é um encadeamento de tweets em que cada item responde ao anterior via in_reply_to_tweet_id. Não existe endpoint "criar thread" — você posta um a um, capturando o ID retornado e injetando no próximo.
A lib oferece um helper v2.tweetThread() que faz o loop por você. Use sempre delay entre tweets (200-500 ms) para não bater rate limit nem ser flagrado como spam.
// src/thread.ts
import { TwitterApi } from "twitter-api-v2";
interface ThreadItem {
text: string;
mediaIds?: string[];
}
export async function postThread(
client: TwitterApi,
items: ThreadItem[]
): Promise<string[]> {
if (items.length === 0) throw new Error("Thread vazia");
const postedIds: string[] = [];
let replyTo: string | undefined;
for (const [i, item] of items.entries()) {
const { data } = await client.v2.tweet({
text: item.text,
...(item.mediaIds && { media: { media_ids: item.mediaIds } }),
...(replyTo && { reply: { in_reply_to_tweet_id: replyTo } }),
});
postedIds.push(data.id);
replyTo = data.id;
// Throttle leve para não disparar anti-spam
if (i < items.length - 1) {
await new Promise(r => setTimeout(r, 300));
}
}
return postedIds;
}
// Uso
const ids = await postThread(client, [
{ text: "1/ Por que automatizar threads no X é uma boa ideia 🧵" },
{ text: "2/ Você posta em horário ótimo sem estar online." },
{ text: "3/ Mede engajamento por item e itera com dados." },
{ text: "4/ E ainda paga $0.04 (4 posts × $0.01) por thread inteira." },
]);
console.log("Tweets criados:", ids);
🛡️ Dica prática
Sempre faça tudo-ou-nada: se o tweet N falhar no meio da thread, DELETE /2/tweets/:id os anteriores. Threads pela metade ficam órfãs e confundem o leitor. Envolva o loop em try/catch e guarde os IDs já postados para rollback.
Conceitos-chave
Liga um tweet ao anterior na thread.
Cada item conhece só seu pai imediato.
Espaço entre posts para evitar bloqueio.
Deletar parciais quando a thread falha.
🎯 Resumo do Módulo
tweet.write, users.read, offline.access).twitter-api-v2 integrado — user context, app-only e refresh automático.media_id obtido e anexado ao tweet v2 com alt text.in_reply_to_tweet_id, throttle e rollback.Próximo Módulo:
3.4 — Próxima rede social: integração e particularidades