# X (Twitter) Poster — Text, Media & Replies via OAuth (`viralanalyzer/x-poster`) Actor

Publish to X through the official X API with your own OAuth authorization. No password, no cookies, no browser automation. You are charged only when a post actually goes out.

- **URL**: https://apify.com/viralanalyzer/x-poster.md
- **Developed by:** [viralanalyzer](https://apify.com/viralanalyzer) (community)
- **Categories:** Social media, Automation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 post publisheds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 𝕏 X (Twitter) Poster — Text, Media & Replies via OAuth

> 🇺🇸 English | [🇧🇷 Português](#português)

> ## ▶️ Start here
>
> **Click Start with the `ViralAnalyzer API Key` field EMPTY.** The Actor replies with your
> personal activation link — that is how you get your key. It takes about 2 minutes, once.
>
> Do **not** sign up on the website first: a normal signup creates the wrong kind of account.

Publish to **X** through the official X API.

This Actor **never asks for your X password**, never stores a session cookie and never
drives a browser. You authorize once with OAuth at [ViralAnalyzer](https://viralanalyzer.com.br),
and the Actor talks to X through that authorization. Nothing here can get your account
flagged for automation, because nothing here pretends to be a human using the site.

### ✨ Features

- 📝 **Text posts** — up to 3000 characters (X's limit for the commentary field)
- 🖼️ **Media** — up to 4 public images or a video per post
- 💬 **Replies** — publish as a reply to an existing post
- 🔍 **Transparent link pricing** — the run tells you which price applied, and why
- 🔐 **Scoped key** — the key you paste here only publishes to X, and you revoke it in one click
- 🧪 **Dry run** — see exactly what would be sent, publish nothing, pay nothing

### 🚀 Setup — how you get your key (once, ~2 minutes)

You do **not** need a ViralAnalyzer account beforehand, and you do **not** create a
X developer app. The Actor issues your activation link for you.

> ⚠️ **Do not sign up on the website.** A normal signup creates a full ViralAnalyzer
> account with a 7-day trial of the whole platform — the wrong kind of account for this.
> Use only the link the Actor gives you: it creates an account scoped to X alone.

**1. Click Start with the `ViralAnalyzer API Key` field EMPTY.**
Leave every other field as you like. The run finishes in a few seconds.

**2. Copy the activation link.** It appears at the top of the run, like this:

> 👉 ATIVE SUA CONTA: https://viralanalyzer.com.br/apify/onboard?token=…

It is also in the run log and in the output record (`activationUrl`). The link is
personal, tied to this Actor, and expires in 3 days.

**3. Open the link and type your e-mail.** You get back a key starting with `va_pub_`.
Copy it — it is shown only once.

**4. Connect X** on the screen right after. You authorize on X's own page;
we never see your password, and you can revoke it whenever you want.

**5. Come back and paste the key into `ViralAnalyzer API Key`.**

> 💡 Start a **new** run — do not try to edit the input of the run that already finished.
> Apify locks secret fields on a completed run, so the field will look greyed out. The key
> field also always looks empty in the form, even after you save it: that is how Apify
> protects secrets, not a bug.

Before the real thing, set `dryRun` to **true** once: the run shows the exact payload that
would be sent, publishes nothing and charges nothing. Then turn it off and publish. From
here on every run publishes — you never repeat steps 1–4.

### 📥 Input

| Field | Type | Required | Notes |
|---|---|---|---|
| `viralanalyzerApiKey` | string (secret) | — | Leave empty on the first run to get your activation link |
| `text` | string | ⚠️ | Required unless `imageUrl` is set. Truncated at 3000 chars by X |
| `imageUrl` | string | — | ONE public https image. Video is not supported yet |
| `linkUrl` | string | — | Publishes as an article card when there is no image |
| `title` | string | — | Card title, used with `linkUrl` |
| `accountId` | integer | — | Leave empty to resolve automatically |
| `dryRun` | boolean | — | Validate without publishing |

### 📤 Output

One record per run. When the post goes out:

```json
{
  "ok": true,
  "provider": "x",
  "accountId": 12,
  "postId": "urn:li:share:7300000000000000000",
  "postUrl": "https://www.x.com/feed/update/urn:li:share:7300000000000000000",
  "publishedPostId": 4471,
  "warnings": [],
  "publishedAt": "2026-09-03T21:04:11.882Z"
}
```

Otherwise you get one labelled record telling you exactly what happened. **The run still
succeeds and you are never charged** — a refusal is an answer, not a failure.

#### Something for you to do — run SUCCEEDS, no charge

| `setup_status` | Meaning | What to do |
|---|---|---|
| `ACTIVATION_REQUIRED` | First run, no key yet | Open the activation link shown at the top of the run |
| `INPUT_REQUIRED` | Neither text nor image given | Fill `text` and/or `imageUrl` |
| `NO_CONNECTED_ACCOUNT` | Key is valid, no X connected | Connect it in the dashboard |
| `MULTIPLE_ACCOUNTS` | Profile *and* Page connected | Copy the `id` you want into `accountId` — the record lists them |
| `ACCOUNT_NOT_FOUND` | `accountId` is not yours | Check the id |
| `INVALID_API_KEY` | Key wrong, revoked or expired | Create a new one |
| `INSUFFICIENT_SCOPE` | Key lacks `social:read`/`social:publish` | Recreate it with both scopes |
| `PROVIDER_NOT_ALLOWED` | Key is locked to another network | Use the key this Actor gave you |
| `NEEDS_REAUTH` | X revoked the authorization | Reconnect in the dashboard |
| `CONTENT_REJECTED` | X refused the content | One image per post, images only, ≤3000 chars |
| `RATE_LIMITED` | X refused by volume | Wait and run again |
| `SUBSCRIPTION_REQUIRED` | ViralAnalyzer plan inactive | Reactivate the plan |
| `SAAS_QUOTA_OR_RATE` | Your ViralAnalyzer quota or 60 req/min | Wait, or upgrade |
| `PUBLISH_IN_PROGRESS` | An identical publish is still running | Check the dashboard before repeating |
| `DRY_RUN` | You asked for a simulation | Nothing — this is the preview |

#### On us — run FAILS, no charge

These never mean you did something wrong. If you see one, the Actor's Issues tab is the
right place to report it.

| Code | Meaning |
|---|---|
| `SAAS_FEATURE_DISABLED` | Social publishing is switched off on our side |
| `SAAS_PROVIDER_NOT_CONFIGURED` | X credentials missing on our side |
| `SAAS_CIPHER_ERROR` | We could not read the stored token |
| `PROVIDER_UNAVAILABLE` | X answered in a way we cannot use |
| `MALFORMED_REQUEST` / `BAD_IDEMPOTENCY_KEY` | Bug in this Actor |
| `UNKNOWN_SAAS_CODE` / `UNCLASSIFIED` / `UNEXPECTED_RESPONSE` | An answer we do not recognise — we would rather fail than guess |
| `NO_CONFIRMATION` | Timeout: we cannot prove the post was created. **Check the dashboard before running again** |

### 💰 Pricing

Two events, because the X API itself charges very differently:

| What you publish | You pay |
|---|---|
| Post **without** a link | **$0.05** |
| Post **with** a link | **$0.30** |

**Why a link costs more.** This is not a markup we invented: X itself bills a post
containing a URL as a different, far more expensive request type — you can see the two
kinds, `PostCreate` and `ContentCreateWithUrl`, in any X developer usage report. We pass
that difference through rather than hiding it in one higher price, which would make every
text-only post subsidise the expensive case.

The output record tells you exactly what was detected and which event was charged
(`containsLink`, `detectedLinks`, `chargedEvent`), and `dryRun` shows the price **before**
you publish.

#### Never charged

| What happens | You pay |
|---|---|
| Activation run (first run, empty key) | **$0** |
| Dry run | **$0** |
| X refused the content, or asked you to wait | **$0** |
| Your key is wrong, expired or lacks scopes | **$0** |
| Something broke on our side | **$0** |

There is **no actor-start fee**: a run that does not publish costs you nothing.

Apify platform usage (compute) is billed to your Apify account on top of the event, as
usual for Actors.

### 🔒 Security

- The Actor never receives your X password or your X token.
- The `va_pub_` key is scoped: it publishes to X and nothing else. It cannot read your
  billing, your media or any other part of your ViralAnalyzer account.
- It is stored hashed (SHA-256) on our side and revocable independently at any time.
- Each run sends an `Idempotency-Key`, so a network hiccup cannot publish your post twice.

### ❓ FAQ

**Does this risk my X account?**
No. This is X's official publishing API with your explicit OAuth authorization — the
same mechanism Buffer or Hootsuite use. What gets accounts restricted is browser automation
with your password, which this Actor does not do.

**Can I post video?**
Not yet. This phase supports text, one image, and article links.

**Can I post to a Company Page?**
Yes — connect the Page in the dashboard and the Actor publishes as the organization. You need
an admin role on that Page.

**What if the run times out?**
It ends as failed and you are not charged. Check the dashboard before running again: a timeout
does not prove the post was not created.

### 📝 Changelog

- **v1.0** (2026-09-03) — first release: text, single image and article posts, profile and
  Company Page, automatic account resolution, dry run, per-post billing.

***

## Português

> ## ▶️ Comece por aqui
>
> **Clique em Start com o campo `ViralAnalyzer API Key` VAZIO.** O Actor devolve seu link
> pessoal de ativação — é assim que você obtém sua chave. Leva cerca de 2 minutos, uma vez só.
>
> **Não** se cadastre pelo site antes: o cadastro comum cria o tipo errado de conta.

Publique no **X** pela API oficial do X.

Este Actor **nunca pede sua senha**, não guarda cookie de sessão e não dirige navegador. Você
autoriza uma vez com OAuth no [ViralAnalyzer](https://viralanalyzer.com.br) e o Actor publica
através dessa autorização. Nada aqui coloca sua conta em risco de bloqueio por automação,
porque nada aqui finge ser uma pessoa usando o site.

### 🚀 Configuração — como você obtém sua chave (uma vez, ~2 minutos)

Você **não** precisa ter conta no ViralAnalyzer antes, e **não** cria app de
desenvolvedor no X. O próprio Actor emite seu link de ativação.

> ⚠️ **Não se cadastre pelo site.** O cadastro comum cria uma conta ViralAnalyzer
> completa, com trial de 7 dias da plataforma inteira — é o tipo errado de conta para
> este Actor. Use apenas o link que o Actor te der: ele cria uma conta restrita ao
> X.

**1. Clique em Start com o campo `ViralAnalyzer API Key` VAZIO.**
Os outros campos podem ficar como você quiser. O run termina em segundos.

**2. Copie o link de ativação.** Ele aparece no topo do run, assim:

> 👉 ATIVE SUA CONTA: https://viralanalyzer.com.br/apify/onboard?token=…

Ele também está no log do run e no registro de saída (`activationUrl`). O link é
pessoal, vinculado a este Actor, e expira em 3 dias.

**3. Abra o link e informe seu e-mail.** Você recebe uma chave que começa com
`va_pub_`. Copie — ela aparece uma única vez.

**4. Conecte o X** na tela seguinte. A autorização acontece na página do próprio
X; nunca vemos sua senha, e você revoga quando quiser.

**5. Volte e cole a chave em `ViralAnalyzer API Key`.**

> 💡 Inicie um run **novo** — não tente editar o input do run que já terminou. A Apify
> bloqueia campos secretos em run finalizado, então o campo aparece esmaecido. O campo da
> chave também sempre parece vazio no formulário, mesmo depois de salvo: é assim que a
> Apify protege segredos, não é bug.

Antes da publicação real, marque `dryRun` como **true** uma vez: o run mostra exatamente o
que seria enviado, não publica nada e não cobra nada. Depois desmarque e publique. Daí em
diante todo run publica — você nunca repete os passos 1–4.

### 📥 Entrada

| Campo | Tipo | Obrigatório | Observações |
|---|---|---|---|
| `viralanalyzerApiKey` | string (secreto) | — | Deixe vazio no primeiro run para receber o link de ativação |
| `text` | string | ⚠️ | Obrigatório se não houver `imageUrl`. O X corta em 3000 caracteres |
| `imageUrl` | string | — | UMA imagem https pública. Vídeo ainda não |
| `linkUrl` | string | — | Sem imagem, vira card de artigo |
| `title` | string | — | Título do card, junto com `linkUrl` |
| `accountId` | integer | — | Vazio = resolve sozinho |
| `dryRun` | boolean | — | Valida sem publicar |

### 📤 Saída

Um registro por run. Quando o post sai, você recebe `ok: true` com `postUrl` e `postId`.

Caso contrário, vem um registro rotulado dizendo exatamente o que aconteceu. **O run
termina com sucesso e você nunca é cobrado** — uma recusa é uma resposta, não uma falha.

#### Algo para você fazer — run com SUCESSO, sem cobrança

| `setup_status` | O que é | O que fazer |
|---|---|---|
| `ACTIVATION_REQUIRED` | Primeiro run, ainda sem chave | Abra o link de ativação no topo do run |
| `INPUT_REQUIRED` | Sem texto e sem imagem | Preencha `text` e/ou `imageUrl` |
| `NO_CONNECTED_ACCOUNT` | Chave válida, X não conectado | Conecte no dashboard |
| `MULTIPLE_ACCOUNTS` | Perfil **e** Página conectados | Copie o `id` desejado para `accountId` — o registro lista os dois |
| `ACCOUNT_NOT_FOUND` | O `accountId` não é seu | Confira o id |
| `INVALID_API_KEY` | Chave errada, revogada ou expirada | Crie outra |
| `INSUFFICIENT_SCOPE` | Falta `social:read`/`social:publish` | Recrie com os dois escopos |
| `PROVIDER_NOT_ALLOWED` | Chave travada em outra rede | Use a chave que este Actor te deu |
| `NEEDS_REAUTH` | O X revogou a autorização | Reconecte no dashboard |
| `CONTENT_REJECTED` | O X recusou o conteúdo | Uma imagem por post, só imagem, ≤3000 caracteres |
| `RATE_LIMITED` | O X recusou por volume | Aguarde e rode de novo |
| `SUBSCRIPTION_REQUIRED` | Plano ViralAnalyzer inativo | Reative o plano |
| `SAAS_QUOTA_OR_RATE` | Sua cota, ou 60 req/min | Aguarde ou faça upgrade |
| `PUBLISH_IN_PROGRESS` | Uma publicação idêntica ainda está rodando | Confira o dashboard antes de repetir |
| `DRY_RUN` | Você pediu simulação | Nada — é a prévia |

#### Nossa responsabilidade — run FALHA, sem cobrança

Nenhum destes significa que você errou. Se aparecer, reporte na aba Issues do Actor.

| Código | O que é |
|---|---|
| `SAAS_FEATURE_DISABLED` | A publicação social está desligada do nosso lado |
| `SAAS_PROVIDER_NOT_CONFIGURED` | Credenciais do X ausentes do nosso lado |
| `SAAS_CIPHER_ERROR` | Não conseguimos ler o token guardado |
| `PROVIDER_UNAVAILABLE` | O X respondeu de forma inutilizável |
| `MALFORMED_REQUEST` / `BAD_IDEMPOTENCY_KEY` | Bug deste Actor |
| `UNKNOWN_SAAS_CODE` / `UNCLASSIFIED` / `UNEXPECTED_RESPONSE` | Resposta que não reconhecemos — preferimos falhar a adivinhar |
| `NO_CONFIRMATION` | Timeout: não conseguimos provar que o post foi criado. **Confira o dashboard antes de rodar de novo** |

### 💰 Preço

Dois eventos, porque a própria API do X cobra de forma muito diferente:

| O que você publica | Você paga |
|---|---|
| Post **sem** link | **$0,05** |
| Post **com** link | **$0,30** |

**Por que link custa mais.** Não é margem que inventamos: o próprio X fatura post com URL
como um tipo de requisição diferente e muito mais caro — os dois tipos, `PostCreate` e
`ContentCreateWithUrl`, aparecem em qualquer relatório de uso de desenvolvedor do X.
Repassamos essa diferença em vez de escondê-la num preço único mais alto, que faria todo
post de texto puro subsidiar o caso caro.

O registro de saída diz exatamente o que foi detectado e qual evento foi cobrado
(`containsLink`, `detectedLinks`, `chargedEvent`), e o `dryRun` mostra o preço **antes** de
publicar.

#### Nunca cobrado

| O que acontece | Você paga |
|---|---|
| Run de ativação (primeiro run, chave vazia) | **$0** |
| Simulação (`dryRun`) | **$0** |
| O X recusou o conteúdo, ou pediu para esperar | **$0** |
| Sua chave está errada, expirada ou sem escopo | **$0** |
| Algo quebrou do nosso lado | **$0** |

**Não há taxa de início**: run que não publica não custa nada.

O uso de plataforma da Apify (compute) é cobrado na sua conta Apify, além do evento.

### 🔒 Segurança

- O Actor nunca recebe sua senha nem seu token do X.
- A chave `va_pub_` é escopada: publica no X e mais nada. Não alcança faturamento,
  mídia nem o resto da sua conta.
- Fica guardada como hash SHA-256 e é revogável isoladamente a qualquer momento.
- Todo run envia `Idempotency-Key`, então uma falha de rede não publica seu post duas vezes.

### ❓ Perguntas frequentes

**Isso arrisca minha conta do X?**
Não. É a API oficial de publicação do X com sua autorização OAuth explícita — o mesmo
mecanismo que Buffer e Hootsuite usam. O que bloqueia conta é automação de navegador com sua
senha, e este Actor não faz isso.

**Posso publicar vídeo?**
Ainda não. Esta fase cobre texto, uma imagem e link de artigo.

**Posso publicar na Página da empresa?**
Sim — conecte a Página no dashboard e o Actor publica como a organização. Você precisa ter
papel de administrador nela.

**E se o run estourar o tempo?**
Termina como falha e você não é cobrado. Confira o dashboard antes de rodar de novo: um
timeout não prova que o post não foi criado.

### 📝 Changelog

- **v1.0** (2026-09-03) — primeira versão: texto, uma imagem e link de artigo, perfil e Página
  da empresa, resolução automática de conta, simulação e cobrança por post.

# Actor input Schema

## `viralanalyzerApiKey` (type: `string`):

Leave EMPTY on your first run: the Actor sends you a one-time activation link and you get a key in about 2 minutes — no developer app, no OAuth setup, no password. After activating, paste the key here (it starts with va\_pub\_). It publishes to X and nothing else, and you can revoke it at any time.

## `text` (type: `string`):

The text of the post. ⚠️ A post containing a link costs more, because the X API charges us 13x for it ($0.200 vs $0.015 per request) — see Pricing. Required unless you attach media.

## `mediaUrls` (type: `array`):

Up to 4 public https URLs of images or video to attach. The URLs must stay reachable while the post is being created; private addresses are refused.

## `replyToPostId` (type: `string`):

Publish as a reply to this X post id (numeric). Leave empty for a standalone post.

## `accountId` (type: `integer`):

Which connected X account to publish as. Leave empty and the Actor resolves it automatically when you have exactly one connected.

## `dryRun` (type: `boolean`):

Validate the input, show exactly what would be sent and which price would apply, without publishing and without being charged.

## Actor input object example

```json
{
  "dryRun": false
}
```

# Actor output Schema

## `results` (type: `string`):

Default dataset with the published post (postUrl, postId, chargedEvent) or one diagnostic record.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("viralanalyzer/x-poster").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("viralanalyzer/x-poster").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call viralanalyzer/x-poster --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,viralanalyzer/x-poster"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/2tTbdzBScFCnNQQuJ/builds/rVtMMTBtSwrnvcVCR/openapi.json
