# X

> Send posts that mention your X account, and your replies, to Dimension Labs with the universal API.

This brings in the public posts that mention your brand's X account, replies to your posts included, and your account's replies to them: each X conversation as one conversation, with the public's posts as customer messages and your replies as your side. You run it on your side: a small script pulls from the X API v2 and posts each message to the [universal API](/api/tracker).

:::note[X's terms]
X's [Developer Policy](https://docs.x.com/developer-terms/policy) limits what X content you may pass to a third party, and asks you to delete or update stored content when it is deleted or changed on X. Check that your agreement with X covers sending post text for analysis.
:::

## What you need

- An X developer account and app, created in the [Developer Console](https://console.x.com), with the Developer Agreement and Policy accepted.
- **Access and cost:** X's self-serve access is **pay-per-use**: you buy credits in the Developer Console and each request draws them down. There is no subscription. X's pricing page lists reads per resource returned, for example $0.005 per Post read, and Owned Reads at $0.001 per resource for your own mentions and posts when the request is made as the account that owns the app. Pay-per-use is capped at 3 million Post reads per monthly billing cycle; above that, X offers Enterprise plans. X says prices are subject to change: check the current rates in the Developer Console before you run a history load.
- Your app's **Bearer Token** and your brand account's numeric user id.
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

## 1. Get API access in X

X's steps are in [Getting access](https://docs.x.com/x-api/getting-started/getting-access).

1. Sign in at [console.x.com](https://console.x.com) with your X account, accept the Developer Agreement and Policy, and complete your profile.
2. Select **New App**, enter a name, description and use case, and generate the credentials. Copy the **Bearer Token** right away: X shows credentials only once.
3. Buy credits in the Developer Console. Set a **Spending limit** for the billing cycle so a large history load can't run up an unexpected bill.
4. Find your account's user id once: `curl "https://api.x.com/2/users/by/username/YOUR_HANDLE" -H "Authorization: Bearer $X_BEARER_TOKEN"`.

The code below uses the Bearer Token (app-only), which is the simplest. X's Owned Read price applies when the request is made in user context as the account that owns the app (OAuth 1.0a with the app's Access Token and Secret, or OAuth 2.0); see X's [authentication docs](https://docs.x.com/resources/fundamentals/authentication) if you want that rate.

## 2. Pull new posts

```bash
curl -G "https://api.x.com/2/users/USER_ID/mentions" -H "Authorization: Bearer $X_BEARER_TOKEN" \
  --data-urlencode "tweet.fields=created_at,author_id,conversation_id,in_reply_to_user_id,lang" \
  --data-urlencode "start_time=2026-10-01T00:00:00Z" --data-urlencode "max_results=100"
```

- `GET /2/users/:id/mentions` returns posts that mention your account, replies and quote posts included. `GET /2/users/:id/tweets` returns your own posts; the code keeps only your replies to other accounts.
- **Incremental:** `start_time` (ISO 8601) returns posts from that time on. `since_id` works too.
- **Pagination:** `max_results` goes from 5 to 100. Pass `meta.next_token` as `pagination_token` until it is missing.
- **How far back:** the mentions timeline returns up to the 800 most recent mentions, and your posts timeline up to 3,200. For older history, X's full-archive search (`GET /2/tweets/search/all`) is available on pay-per-use.
- `conversation_id` is the id of the post that started the thread, so it groups a complaint and your reply in one conversation.
- **Rate limits (X's):** mentions allows 450 requests per 15 minutes per app, your posts timeline 10,000. A `429` means wait until the `x-rate-limit-reset` time.
- **Cost:** every Post returned is billed. X doesn't charge twice for the same Post within one UTC day, so a re-run on the same day is not billed again (X calls this a soft guarantee).

## 3. Map the fields

Each X conversation is one session. Posts by other accounts are incoming; your account's replies are outgoing.

| Dimension Labs | X | Notes |
|---|---|---|
| `text` | `text` | |
| `userId` | `author_id` | X's numeric user id, prefixed: `x-2244994945`. Don't send the username or display name. |
| `incoming` | `author_id` | `false` when `author_id` is your account, `true` otherwise. |
| `sessionId` | `conversation_id` | Prefix it, for example `x-1225470895902412800`. |
| `conversationId` | `conversation_id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `created_at` | ISO 8601. Convert with `toMillis`. |
| `platformJson` | post `id`, `lang`, `in_reply_to_user_id`, which timeline it came from | The post `id` is what you need to honor a deletion on X. |

**Personal data.** Posts often hold @handles, names and order numbers: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// x.mjs. Environment: X_BEARER_TOKEN, X_USER_ID (your brand account's numeric id), DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = 'https://api.x.com/2';
const ME = process.env.X_USER_ID;

// Retries X's rate limits (429) and server errors (5xx). On a 429 it waits until x-rate-limit-reset (or Retry-After).
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.X_BEARER_TOKEN}` } });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`X returned ${res.status}: ${await res.text()}`);
    const reset = Number(res.headers.get('x-rate-limit-reset')) * 1000 - Date.now();
    await sleep(Number(res.headers.get('retry-after')) * 1000 || (reset > 0 ? reset + 1000 : 2 ** attempt * 1000));
  }
}

// One timeline (mentions or your own posts), from `since` on, every page.
async function* timeline(kind, since) {
  let token;
  do {
    const q = new URLSearchParams({ max_results: '100', 'tweet.fields': 'created_at,author_id,conversation_id,in_reply_to_user_id,lang',
      start_time: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z'), ...(token && { pagination_token: token }) });
    const page = await get(`${API}/users/${ME}/${kind}?${q}`);
    yield page.data ?? [];
    token = page.meta?.next_token;
  } while (token);
}

// Sends posts that mention your account, and your replies to other accounts, created at or after `since` (Unix ms).
// Returns the new `since`.
export async function syncX({ since, historical = false }) {
  let newest = since;
  for (const kind of ['mentions', 'tweets']) {
    for await (const posts of timeline(kind, since)) {
      for (const p of posts) {
        const mine = p.author_id === ME;
        if (kind === 'mentions' && mine) continue; // your own posts come from the second timeline
        if (kind === 'tweets' && (!p.in_reply_to_user_id || p.in_reply_to_user_id === ME)) continue; // keep replies to others only
        if (!p.text?.trim()) continue;
        const ts = toMillis(p.created_at);
        newest = Math.max(newest, ts);
        const sessionId = `x-${p.conversation_id ?? p.id}`;
        await sendToDimensionLabs({
          text: p.text.trim(),
          userId: `x-${p.author_id}`,
          incoming: !mine,
          sessionId,
          conversationId: sessionId,
          dimensionlabs_timestamp: ts,
          platformJson: { postId: p.id, lang: p.lang, inReplyToUserId: p.in_reply_to_user_id, timeline: kind },
        }, { historical: historical || ts < Date.now() - HOUR });
      }
    }
  }
  return newest;
}
```

The first timeline sends mentions; the second sends your replies into the same conversations. Your own posts that aren't replies are skipped: on their own, they have no customer words to enrich.

## 5. Run it

Save the code as `x.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/scheduled-sync) runner: `import { syncX as sync } from './x.mjs';`.

- **History first:** `node run.mjs 2026-01-01` sends every mention and reply the timelines still return since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends new posts to `/track`, and they are enriched automatically every day. Anything more than an hour old when the job sees it goes to `/trackhistorical`, and needs a backfill.
- **Re-runs are safe:** duplicates are removed automatically.

**Deletions.** X asks you to delete or update stored posts that are deleted or changed on X, as soon as reasonably possible, or within 24 hours of a request from X or the account owner. X's [batch compliance](https://docs.x.com/x-api/compliance/batch-compliance/introduction) endpoints tell you which of your stored post ids were deleted or changed. You can't delete individual records in Dimension Labs yourself: email support@dimensionlabs.io with the source and the post ids for a targeted deletion.

If you have posts in a file instead, [upload it](/data/upload-a-file) and check it first with the [file checker](/data/check-a-file), then run a backfill. The same X terms apply to a file.

## X reference

- [Getting access](https://docs.x.com/x-api/getting-started/getting-access)
- [Pay-per-usage pricing and credits](https://docs.x.com/x-api/getting-started/pricing)
- [Timelines](https://docs.x.com/x-api/posts/timelines/introduction)
- [Get mentions](https://docs.x.com/x-api/users/get-mentions)
- [Get posts](https://docs.x.com/x-api/users/get-posts)
- [Search Posts](https://docs.x.com/x-api/posts/search/introduction)
- [Rate limits](https://docs.x.com/x-api/fundamentals/rate-limits)
- [Batch compliance](https://docs.x.com/x-api/compliance/batch-compliance/introduction)
- [Developer Policy](https://docs.x.com/developer-terms/policy)
- [Developer Agreement](https://docs.x.com/developer-terms/agreement)

Checked against X's documentation on 2026-10-26.
