Want to get tweets with JavaScript in 2026? The short answer: you call the X API v2 (Twitter is now X) from a small server-side proxy that holds your credentials, and your front-end fetch()es tweets from your own endpoint — never directly from api.twitter.com. The old tricks from the original version of this post are dead: API v1.1 statuses endpoints are retired, the unauthenticated cdn.syndication.twimg.com widget JSON is gone, and read access now needs an authenticated, paid API tier. If you only need to show one tweet, skip the API entirely and use the oEmbed embedded-Tweet widget.
Key takeaways
- Don't call the X API from browser JavaScript. It would leak your bearer token and is blocked by CORS anyway. Use a server-side proxy.
- v1.1 is gone, v2 is the API. Read endpoints live under
https://api.twitter.com/2/and require authentication (an App-Only Bearer token for public reads, OAuth 2.0 PKCE for user-context reads). - Reads need a paid tier. The Free tier is effectively write-only; Basic, Pro, and Enterprise unlock meaningful tweet reads (named here, with no prices).
- Just one tweet to display? Use the oEmbed / embedded-Tweet widget from
platform.twitter.com— no API key, no token, no proxy. - Use a client if you like. The community
twitter-api-v2npm package wraps v2 nicely, but the architecture (server holds the token) stays the same.
Why did the old tweet-fetching code stop working?
The original 2014 approach in this article fetched a JSON timeline from cdn.syndication.twimg.com/widgets/timelines/<widgetId> and scraped the markup with JavaScript. That worked because Twitter exposed an unauthenticated widget feed and a public v1.1 statuses/user_timeline endpoint. Both are gone. Twitter rebranded to X, retired the v1.1 statuses endpoints, removed the legacy widget/syndication JSON, and moved everything behind the authenticated X API v2 with metered, paid access tiers.
| Aspect | Old way (v1.1 / widget JSON) | Current way (X API v2) |
|---|---|---|
| Base URL | api.twitter.com/1.1/ · cdn.syndication.twimg.com |
api.twitter.com/2/ |
| Auth for reads | Often unauthenticated / scraped | Required — App-Only Bearer or OAuth 2.0 PKCE |
| Timeline endpoint | statuses/user_timeline |
GET /2/users/:id/tweets |
| Search | search/tweets |
GET /2/tweets/search/recent |
| Cost | Free, unlimited-ish | Paid tier (Free is write-only) |
| Where you call it | Straight from the browser | Server-side only |
| Status | Retired | Supported |
What is the right architecture for showing tweets with JavaScript?
There are three sane patterns in 2026. Pick by what you actually need.
| Pattern | Token in browser? | Needs API tier? | Use it when |
|---|---|---|---|
| Client-side direct to X API ❌ | Yes — leaks your secret, blocked by CORS | Yes | Never. Don't do this. |
| Server-side proxy ✅ | No — token stays on the server | Yes | You need a custom-styled feed, search, or filtering |
| oEmbed / embedded widget ✅ | No token at all | No | You just want to drop in one tweet as-is |
The proxy pattern is the workhorse: a tiny Node service holds the bearer token, calls GET /2/users/:id/tweets (or recent search), optionally caches the result, and exposes a clean /api/tweets route on your origin. Your front-end JavaScript only ever talks to your own domain — no token, no CORS headaches.
// server.js — Node 18+ (global fetch built in). The bearer token NEVER leaves the server.
import express from 'express';
const app = express();
const BEARER = process.env.X_BEARER_TOKEN; // App-Only Bearer token, server-side only
// Cache so you don't burn your read quota on every page view.
const cache = new Map();
const TTL = 5 * 60 * 1000; // 5 minutes
async function xGet(path, params = {}) {
const url = new URL(`https://api.twitter.com/2/${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: `Bearer ${BEARER}` } });
if (!res.ok) throw new Error(`X API ${res.status}: ${await res.text()}`);
return res.json();
}
// GET /api/tweets?user=MicroPyramid -> that handle's latest posts
app.get('/api/tweets', async (req, res) => {
const handle = String(req.query.user || 'MicroPyramid').replace(/[^A-Za-z0-9_]/g, '');
const key = `tl:${handle}`;
const hit = cache.get(key);
if (hit && Date.now() - hit.at < TTL) return res.json(hit.data);
try {
// 1) resolve the numeric user id from the @handle
const { data: user } = await xGet(`users/by/username/${handle}`);
// 2) pull recent posts with just the fields the UI needs
const timeline = await xGet(`users/${user.id}/tweets`, {
max_results: '10',
'tweet.fields': 'created_at,public_metrics,entities',
});
cache.set(key, { at: Date.now(), data: timeline });
res.json(timeline);
} catch (err) {
res.status(502).json({ error: String(err) });
}
});
app.listen(3000, () => console.log('Tweet proxy on http://localhost:3000'));Can I use the official client instead of raw fetch?
Yes. The community twitter-api-v2 npm package is the most popular X API v2 client for Node and handles pagination, typing, and rate-limit metadata for you. The architecture is identical — it still runs on the server with your token.
// npm i twitter-api-v2
import { TwitterApi } from 'twitter-api-v2';
// App-Only (read) context — perfect behind your proxy route.
const client = new TwitterApi(process.env.X_BEARER_TOKEN);
const x = client.readOnly.v2;
export async function recentTweets(handle = 'MicroPyramid') {
const user = await x.userByUsername(handle);
const timeline = await x.userTimeline(user.data.id, {
max_results: 10,
'tweet.fields': ['created_at', 'public_metrics', 'entities'],
});
return timeline.data.data ?? []; // array of tweet objects
}How does the browser actually render the tweets?
The front-end stays dumb and safe: it fetch()es your /api/tweets route — same origin, no token, no CORS config — and renders the JSON. Build each tweet with textContent (not innerHTML) so tweet text can't inject markup into your page.
// app.js — runs in the browser. Talks ONLY to your own origin.
async function renderTweets(handle = 'MicroPyramid') {
const box = document.querySelector('#tweets');
const res = await fetch(`/api/tweets?user=${encodeURIComponent(handle)}`);
if (!res.ok) {
box.textContent = 'Could not load posts right now.';
return;
}
const { data: tweets = [] } = await res.json();
box.replaceChildren(
...tweets.map((t) => {
const el = document.createElement('article');
const when = new Date(t.created_at).toLocaleDateString();
el.textContent = `${t.text} — ${when}`; // textContent = no HTML injection
return el;
}),
);
}
renderTweets();How do I just embed one tweet without any API key?
If your goal is simply to display a specific tweet exactly as X renders it, you don't need the API, a token, or a proxy at all. Use the embedded-Tweet widget: paste a blockquote linking to the tweet and load platform.twitter.com/widgets.js. X's script swaps it for a fully-styled, interactive card. You can generate the markup at publish.twitter.com, or fetch it server-side from the oEmbed endpoint (https://publish.twitter.com/oembed?url=...) and cache the returned HTML.
<!-- The no-API way to show ONE tweet. Drop it where you want the card. -->
<blockquote class="twitter-tweet">
<a href="https://twitter.com/MicroPyramid/status/1234567890123456789"></a>
</blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>Which X API v2 endpoints and access tier do I need?
Match the endpoint to the job, and remember that reads require a paid tier — the Free tier is essentially write-only.
| You want to... | Endpoint | Auth |
|---|---|---|
| Look up a tweet by id | GET /2/tweets |
App-Only Bearer |
| Show a user's recent posts | GET /2/users/:id/tweets |
App-Only Bearer |
| Resolve @handle → numeric id | GET /2/users/by/username/:name |
App-Only Bearer |
| Search recent public tweets | GET /2/tweets/search/recent |
App-Only Bearer |
| Read on behalf of a logged-in user | user-context endpoints | OAuth 2.0 PKCE |
The read tiers, smallest to largest, are Free (write-only in practice), Basic, Pro, and Enterprise — each raising your monthly read allowance and rate limits. Choose based on how many posts you pull and how often you refresh; aggressive caching on your proxy keeps you on a smaller tier.
This pairs naturally with a backend. If your site runs on a server framework, the read-and-cache logic lives there — see our companion guide on how to integrate the Twitter/X social API into a Django app for the server-side, OAuth, and caching specifics. MicroPyramid has built and maintained social and third-party API integrations across 50+ projects since 2014 (12+ years); if you'd rather hand off the proxy, auth, and front-end, our web development team ships these in days, not weeks.
Frequently Asked Questions
Can I fetch tweets purely with client-side JavaScript?
No. Calling the X API v2 from the browser would expose your bearer token to anyone who opens DevTools, and X's API doesn't send CORS headers for browser origins, so the request fails anyway. The supported pattern is a server-side proxy that holds the token and exposes a clean endpoint your front-end fetch()es.
Does the X (Twitter) API still have a free option for reading tweets?
Not really. The Free tier is effectively write-only and is meant for posting, not pulling timelines. Meaningful read access — user timelines and recent search — starts at the Basic tier, with Pro and Enterprise above it for higher volume. Tiers are named here without prices; check developer.x.com for current figures.
What replaced the old v1.1 statuses/user_timeline endpoint?
GET /2/users/:id/tweets is the v2 equivalent. Because v2 keys on the numeric user id, you first call GET /2/users/by/username/:name to turn an @handle into an id, then request that id's timeline. The v1.1 statuses endpoints and the old cdn.syndication.twimg.com widget JSON are both retired.
How do I display just one tweet without dealing with the API?
Use the embedded-Tweet widget. Paste a blockquote class="twitter-tweet" linking to the tweet and load platform.twitter.com/widgets.js; X renders a styled, interactive card. No API key, token, or proxy needed — you can generate the snippet at publish.twitter.com or fetch it from the oEmbed endpoint.
Should I use the twitter-api-v2 npm package or raw fetch?
Both work. Raw fetch keeps dependencies to zero and is fine for one or two endpoints. The community twitter-api-v2 client is worth it once you need pagination, rate-limit metadata, or OAuth 2.0 PKCE user-context auth — it runs server-side with your token, so the secure architecture doesn't change.
How do I avoid hitting X API rate limits?
Cache aggressively on your proxy. Store each timeline or search response for a few minutes and serve readers from cache instead of calling X on every page view — the example server keeps a 5-minute in-memory cache. For multi-instance deployments, push the cache to Redis. Caching both protects you from rate limits and lets you stay on a smaller, cheaper tier.