Integration Guide
Twitter/X API Code Examples: Python, Node.js, TypeScript, Go
Working Twitter/X API examples in Python, Node.js, TypeScript, Go and Bun: search tweets, read timelines, replies, profiles and followers, paginate and handle errors, with no X developer account.
Written by Alex P.
- twitter API
- X API
- twitter API python
- twitter API nodejs
- twitter API typescript
- twitter API go
- bun
- code examples
Short answer: you can read public Twitter/X data from any language with one HTTP request. FetchLayer’s Twitter/X API takes a POST with a JSON body and your key, and returns tweets, timelines, replies, profiles and follower lists as JSON. You don’t need an X developer account. It costs $1.99 per 1,000 requests with 30 free to start, and the same data is available to AI agents over MCP at https://mcp.fetchlayer.dev. Every example below was run against the live API on 4 November 2026.
Jump to your language: Python · Node.js · TypeScript · Go · Bun
How every request works
All five languages make the same call:
- URL:
POST https://api.fetchlayer.dev/twitter/<endpoint> - Headers:
Authorization: Bearer ss-your-fetchlayer-keyandContent-Type: application/json - Body: the endpoint’s parameters as JSON, for example
{"query": "postgres performance", "product": "Latest"} - Response: JSON. A tweet carries
id,createdAt,text,author(withhandleandfollowersCount),counts(likes,reposts,replies,views) andurl. Lists returnnextCursorfor the next page.
Get a key and export it as FETCHLAYER_API_KEY; every example reads it from there.
The endpoints you’ll use most
| Endpoint | Body | Returns |
|---|---|---|
search | query, product (Top, Latest, People, Media, Lists) | tweets (or profiles for People) |
user-tweets | handle, optional cursor | An account’s recent tweets |
tweet-detail | tweetId | One tweet |
tweet-replies | tweetId, optional cursor | replies to a tweet |
user-profile-details | handle | The account’s profile |
followers, following | handle, optional cursor | profiles, about 50 to 70 per page |
All 11 endpoints and every field are in the Twitter/X API reference.
Errors are the same everywhere
Every error has the same body:
{"error":"Missing required parameter: handle","status":400,"code":"invalid_input","retryable":false,"requestId":"…"}
Retry only when retryable is true (503 temporarily_unavailable or time_limit, 502 upstream_error, 500 internal_error), with a backoff. A 400 means fix the request, 401 check the key, 402 top up, 429 slow down. Failed requests are never billed.
Python
requests is all you need. This searches, walks an account’s timeline across two pages, and reads one page of its followers:
import os
import time
import requests
API = "https://api.fetchlayer.dev/twitter"
HEADERS = {"Authorization": f"Bearer {os.environ['FETCHLAYER_API_KEY']}"}
def call(endpoint, body, attempts=3):
"""POST to an endpoint; retry only what the API marks retryable."""
for attempt in range(attempts):
res = requests.post(f"{API}/{endpoint}", json=body, headers=HEADERS, timeout=90)
if res.ok:
return res.json()
err = res.json()
if not err.get("retryable") or attempt == attempts - 1:
raise RuntimeError(f"{err['code']}: {err['error']} (request {err['requestId']})")
time.sleep(2 ** attempt)
# Latest tweets matching a keyword
results = call("search", {"query": "postgres performance", "product": "Latest"})
for t in results["tweets"][:5]:
print(f"@{t['author']['handle']}: {t['text'][:60]!r} ({t['counts']['likes']} likes)")
# An account's timeline, page by page: pass nextCursor back as cursor.
# A pinned tweet can appear on more than one page, so key by id.
def timeline(handle, pages):
seen, cursor = {}, None
for _ in range(pages):
body = {"handle": handle}
if cursor:
body["cursor"] = cursor
data = call("user-tweets", body)
for t in data["tweets"]:
seen[t["id"]] = t
cursor = data.get("nextCursor")
if not cursor:
break
return list(seen.values())
tweets = timeline("bunjavascript", pages=2)
top = max(tweets, key=lambda t: t["counts"]["views"] or 0)
print(len(tweets), "tweets; most viewed:", top["counts"]["views"], top["url"])
# Who follows an account (one page)
page = call("followers", {"handle": "bunjavascript"})
biggest = sorted(page["profiles"], key=lambda p: p["followersCount"] or 0, reverse=True)[:3]
print([f"@{p['handle']} ({p['followersCount']})" for p in biggest])
Output from the run (trimmed):
@petereliaskraft: 'Fun optimization story!\n\nWhen we were scaling DBOS to suppor' (23 likes)
51 tweets; most viewed: 4736824 https://x.com/bunjavascript/status/1995916660847640934
['@wesbos (446664)', '@theo (420128)', '@kentcdodds (334959)']
Paginating followers
followers and following return one page per request; X sets the page size, about 50 to 70 accounts per page. Use nextCursor for more: pass it back as cursor with the same other parameters.
import os
import requests
API = "https://api.fetchlayer.dev/twitter"
HEADERS = {"Authorization": f"Bearer {os.environ['FETCHLAYER_API_KEY']}"}
def followers(handle, pages):
"""Yield an account's followers, page by page: pass nextCursor back as cursor."""
cursor = None
for _ in range(pages):
body = {"handle": handle}
if cursor:
body["cursor"] = cursor
data = requests.post(f"{API}/followers", json=body, headers=HEADERS, timeout=90).json()
yield from data["profiles"]
cursor = data.get("nextCursor")
if not cursor:
break
people = list(followers("bunjavascript", pages=3))
print(len(people), "followers, unique:", len({p["id"] for p in people}) == len(people))
On our run, three pages returned 169 unique followers. count has no effect on these endpoints; it only changes the page size on search (at most 20) and verified-followers (at most 100).
Coming from Tweepy? Tweepy wraps X’s official API, which since February 2026 is pay-per-use: $0.005 per post read and $0.010 per user or follower read (X’s pricing), and it needs an X developer account. The calls above need neither. Our Twitter scraper API comparison covers the trade-offs.
Node.js
Node 18+ has fetch built in. AbortSignal.timeout bounds each request, and a small pool fetches several accounts without a dependency:
const API = 'https://api.fetchlayer.dev/twitter';
const KEY = process.env.FETCHLAYER_API_KEY;
async function call(endpoint, body, { attempts = 3, timeoutMs = 90_000 } = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(`${API}/${endpoint}`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeoutMs),
});
if (res.ok) return res.json();
const err = await res.json();
if (!err.retryable || attempt === attempts - 1) {
throw new Error(`${err.code}: ${err.error} (request ${err.requestId})`);
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
}
// The top tweet on a topic, and what people replied to it
const { tweets } = await call('search', { query: 'bun 1.4', product: 'Top' });
const top = tweets[0];
console.log(`@${top.author.handle}: ${top.text.slice(0, 80)}`);
const thread = await call('tweet-replies', { tweetId: top.id });
for (const reply of thread.replies.slice(0, 3)) {
console.log(` ↳ @${reply.author.handle}: ${reply.text.slice(0, 70)}`);
}
// Several accounts at once, at most 2 requests in flight
async function mapLimit(items, limit, fn) {
const results = new Array(items.length);
let next = 0;
await Promise.all(
Array.from({ length: limit }, async () => {
while (next < items.length) {
const i = next++;
results[i] = await fn(items[i]);
}
}),
);
return results;
}
const handles = ['bunjavascript', 'nodejs', 'deno_land'];
const profiles = await mapLimit(handles, 2, (handle) => call('user-profile-details', { handle }));
for (const { profile } of profiles) {
console.log(`@${profile.handle}: ${profile.followersCount} followers`);
}
Run with node tw.mjs. It printed the top tweet on the topic, three replies to it, and each account’s follower count. If you’d rather not write the HTTP layer, the @fetchlayer/twitter npm package wraps every endpoint.
TypeScript
Type the parts you rely on and check them at runtime: a TypeScript type describes the shape, it doesn’t validate it. Note that followersCount and views can be null.
export {};
interface Author {
id: string;
handle: string;
name: string;
followersCount: number | null;
verified: boolean;
}
interface Tweet {
id: string;
createdAt: string;
text: string;
author: Author;
counts: { likes: number; reposts: number; replies: number; views: number | null };
url: string;
}
interface SearchResult {
tweets: Tweet[];
nextCursor: string | null;
}
interface ApiError {
error: string;
code: string;
retryable: boolean;
requestId: string;
}
class FetchLayerError extends Error {
constructor(public detail: ApiError) {
super(`${detail.code}: ${detail.error}`);
}
}
async function call<T>(endpoint: string, body: Record<string, unknown>): Promise<T> {
const res = await fetch(`https://api.fetchlayer.dev/twitter/${endpoint}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FETCHLAYER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) throw new FetchLayerError((await res.json()) as ApiError);
return (await res.json()) as T;
}
// Types describe the shape; check the fields you depend on at runtime.
function isTweet(value: unknown): value is Tweet {
const t = value as Tweet;
return typeof t?.id === 'string' && typeof t?.text === 'string' && typeof t?.author?.handle === 'string';
}
const { tweets } = await call<SearchResult>('search', { query: 'typescript 6', product: 'Latest' });
for (const t of tweets.filter(isTweet).slice(0, 5)) {
console.log(`@${t.author.handle} (${t.author.followersCount ?? '?'}): ${t.text.slice(0, 60)}`);
}
try {
await call('user-tweets', {});
} catch (e) {
if (e instanceof FetchLayerError) console.log('handled:', e.detail.code, e.detail.retryable);
}
Runs as-is with Bun (bun tw.ts) and passes tsc --strict with @types/node; for Node, compile it with tsc first. The last lines catch an error as a typed FetchLayerError.
Go
The standard library is enough. A context bounds the whole run, and a sync.WaitGroup reads three timelines at once:
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"sync"
"time"
)
type Tweet struct {
ID string `json:"id"`
Text string `json:"text"`
URL string `json:"url"`
Author struct {
Handle string `json:"handle"`
} `json:"author"`
Counts struct {
Likes int `json:"likes"`
Views *int `json:"views"`
} `json:"counts"`
}
type Timeline struct {
Tweets []Tweet `json:"tweets"`
NextCursor *string `json:"nextCursor"`
}
type APIError struct {
Error string `json:"error"`
Code string `json:"code"`
Retryable bool `json:"retryable"`
RequestID string `json:"requestId"`
}
func call(ctx context.Context, endpoint string, body any, out any) error {
payload, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.fetchlayer.dev/twitter/"+endpoint, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("FETCHLAYER_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
var apiErr APIError
json.NewDecoder(res.Body).Decode(&apiErr)
return fmt.Errorf("%s: %s (retryable=%v, request %s)", apiErr.Code, apiErr.Error, apiErr.Retryable, apiErr.RequestID)
}
return json.NewDecoder(res.Body).Decode(out)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
handles := []string{"golang", "rustlang", "ziglang"}
timelines := make([]Timeline, len(handles))
errs := make([]error, len(handles))
var wg sync.WaitGroup
for i, handle := range handles {
wg.Add(1)
go func(i int, handle string) {
defer wg.Done()
errs[i] = call(ctx, "user-tweets", map[string]any{"handle": handle}, &timelines[i])
}(i, handle)
}
wg.Wait()
for i, handle := range handles {
if errs[i] != nil {
fmt.Println("@"+handle, "failed:", errs[i])
continue
}
best := Tweet{}
for _, t := range timelines[i].Tweets {
if t.Counts.Likes > best.Counts.Likes {
best = t
}
}
fmt.Printf("@%s: %d tweets, most liked (%d): %s\n", handle, len(timelines[i].Tweets), best.Counts.Likes, best.URL)
}
}
go run tw.go printed each account’s most-liked recent tweet. Use golang.org/x/sync/errgroup instead of the WaitGroup if you want the first error to cancel the rest.
Bun
Bun’s built-in SQLite and HTTP server make a keyword monitor one file. Run it on a schedule: INSERT OR IGNORE on the tweet id stores only what’s new.
import { Database } from 'bun:sqlite';
const db = new Database('tweets.sqlite');
db.run(`CREATE TABLE IF NOT EXISTS tweets (
id TEXT PRIMARY KEY, handle TEXT, text TEXT, likes INTEGER, url TEXT, created_at TEXT
)`);
const insert = db.prepare(
'INSERT OR IGNORE INTO tweets (id, handle, text, likes, url, created_at) VALUES (?, ?, ?, ?, ?, ?)',
);
async function poll(query: string) {
const res = await fetch('https://api.fetchlayer.dev/twitter/search', {
method: 'POST',
headers: {
Authorization: `Bearer ${Bun.env.FETCHLAYER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, product: 'Latest' }),
});
if (!res.ok) throw new Error(JSON.stringify(await res.json()));
const { tweets } = await res.json();
let added = 0;
for (const t of tweets) {
added += insert.run(t.id, t.author.handle, t.text, t.counts.likes, t.url, t.createdAt).changes;
}
return added;
}
console.log('new tweets stored:', await poll('"bun install"'));
// Serve what you stored
const server = Bun.serve({
port: 3000,
fetch() {
const rows = db.query('SELECT handle, text, likes, url FROM tweets ORDER BY rowid DESC LIMIT 50').all();
return Response.json(rows);
},
});
const rows = await (await fetch(`http://localhost:${server.port}/`)).json();
console.log('served', rows.length, 'rows; first from @' + rows[0]?.handle);
server.stop();
On our run it stored 20 new tweets and served them back as JSON.
What it costs
Every language costs the same: $1.99 per 1,000 requests pay-as-you-go, less on monthly plans, and the first 30 requests are free (pricing). A request returns a page of results, so a timeline page of about 25 tweets costs one request. On X’s own API, 25 post reads cost $0.125. Failed requests are never billed.
Related guides
- How to Scrape Twitter/X in 2026: every method, compared
- Best Twitter/X scraper APIs: prices and endpoints side by side
- Export Twitter followers to CSV
- Monitor Twitter keywords
- Twitter/X MCP server: the same data inside Claude, Cursor and other agents