Post to Twitter from Node.js
with one fetch call
No OAuth 1.0a. No request signing. No X developer account. Just fetch, a Bearer key, and a JSON body.
7-day free trial • No credit card required
No X OAuth 1.0a to build
To post to Twitter from Node.js, send a POST request to OpenTweet's REST API athttps://opentweet.io/api/v1/postswith your API key as a Bearer token and the tweet text as JSON. Node 18 and newer ship with a global fetch, so there is nothing to install.
Going straight to the X API from Node means an approved developer account, OAuth 1.0a consumer keys, access tokens, and manual HMAC-SHA1 request signing on every call. That signature code is where most tutorials go wrong, and it is a lot of ceremony just to post one tweet.
OpenTweet replaces all of that with a single ot_ key, JSON payloads, and built-in scheduling. No developer portal application, no OAuth token exchange, no callback URLs, and no signing.
// The old way (X API + OAuth 1.0a)
import crypto from "crypto";
import OAuth from "oauth-1.0a";
// Step 1: approved X developer account
// Step 2: consumer + access token pairs
const oauth = new OAuth({
consumer: { key: "...", secret: "..." },
signature_method: "HMAC-SHA1",
hash_function(base, key) {
return crypto
.createHmac("sha1", key)
.update(base)
.digest("base64");
},
});
// Step 3: sign every request by hand,
// then still build your own scheduler.// The new way (OpenTweet API)
await fetch("https://opentweet.io/api/v1/posts", {
method: "POST",
headers: {
Authorization: "Bearer ot_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Hello from Node.js!",
scheduled_date: "2026-07-10T09:00:00Z",
}),
});
// Done. Scheduled. No OAuth. No signing.Step-by-Step: Node.js to Twitter
Get an ot_ API key
Sign up for OpenTweet, connect your X account, and open the developer page at opentweet.io/developer. Generate an API key. It starts with ot_ and is the only credential you need. Store it in an environment variable, never in your source.
# Store your key as an environment variable
export OPENTWEET_API_KEY="ot_your_key"
# Sanity check the key with GET /me
curl https://opentweet.io/api/v1/me \
-H "Authorization: Bearer $OPENTWEET_API_KEY"Post your first tweet
Send a POST to /api/v1/posts with a JSON body. Set publish_now to true to send it live right away. This uses the global fetch on Node 18+.
const res = await fetch("https://opentweet.io/api/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Hello from Node.js!",
publish_now: true,
}),
});
console.log(res.status); // 201 = created
console.log(await res.json());Schedule a tweet for later
Swap publish_now for a scheduled_date in ISO 8601 format. The two fields are mutually exclusive. Omit both and the post is saved as a draft instead.
await fetch("https://opentweet.io/api/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Scheduled from Node.js!",
scheduled_date: "2026-07-10T09:00:00Z",
}),
});publish_now and scheduled_date are exclusive
Send one or the other, never both in the same request. To save a draft you can edit later, leave both out and OpenTweet stores the post unscheduled.Post a thread
Set is_thread to true and pass thread_tweets as an array of strings. OpenTweet chains them into a native X thread. Add publish_now or scheduled_date the same way.
await fetch("https://opentweet.io/api/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
is_thread: true,
thread_tweets: [
"Thread: shipping to X from Node in one call.",
"No OAuth 1.0a, no request signing, no dev account.",
"Just fetch, a Bearer key, and JSON. That's it.",
],
thread_media: [[], [], []],
publish_now: true,
}),
});Handle 401, 403, and 429
fetch does not throw on HTTP errors, so always check response.ok and the status. 401 means a missing or bad key, 403 means your plan or account cannot perform the action, and 429 means you hit a rate limit. The JSON body carries the reason.
const res = await fetch("https://opentweet.io/api/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Hi", publish_now: true }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
if (res.status === 401) throw new Error("Bad or missing ot_ key");
if (res.status === 403) throw new Error("Plan or account cannot post: " + (err.error || ""));
if (res.status === 429) throw new Error("Rate limited, retry after a pause");
throw new Error(`OpenTweet ${res.status}: ${err.error || res.statusText}`);
}Wrap it in a reusable postTweet() helper
Pull the fetch into one async postTweet() function that sets the Authorization header once and throws on non-2xx responses. Now any part of your app can just await postTweet(...).
const BASE = "https://opentweet.io/api/v1";
const KEY = process.env.OPENTWEET_API_KEY;
export async function postTweet(text, options = {}) {
const res = await fetch(`${BASE}/posts`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, ...options }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`OpenTweet ${res.status}: ${err.error || res.statusText}`);
}
return res.json();
}
// Publish now
await postTweet("Shipped it.", { publish_now: true });
// Schedule for later
await postTweet("Good morning X.", {
scheduled_date: "2026-07-10T09:00:00Z",
});Batch schedule up to 50 at once
Create posts as drafts first, collect their ids, then hand them to the batch-schedule endpoint. One request instead of fifty.
import { postTweet } from "./opentweet.mjs";
const BASE = "https://opentweet.io/api/v1";
const HEADERS = {
Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
"Content-Type": "application/json",
};
const texts = [
"Monday: ship something today.",
"Tuesday: write the test first.",
"Wednesday: simplicity beats cleverness.",
];
// Create drafts (omit publish_now and scheduled_date) and collect ids
const ids = [];
for (const text of texts) {
const draft = await postTweet(text); // saved as a draft
ids.push(draft.id);
}
// Schedule them at 9:00 UTC on consecutive days
const start = new Date("2026-07-10T09:00:00Z");
const schedules = ids.map((post_id, i) => ({
post_id,
scheduled_date: new Date(start.getTime() + i * 86400000).toISOString(),
}));
const res = await fetch(`${BASE}/posts/batch-schedule`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ schedules }),
});
console.log(`Scheduled ${schedules.length} tweets`, res.status);Pro Tips
Keep the key in the environment
Read process.env.OPENTWEET_API_KEY instead of hardcoding your ot_ key. Add it to a local .env and to your CI or host secrets for production.
Verify the key at startup
Call GET /me once when your service boots. A 200 confirms the key is live before you try to post, so you fail fast on a bad or revoked key.
List what is queued
GET /posts?status=scheduled returns everything waiting to go out. Handy for a dashboard, a health check, or a nightly report of the week ahead.
Pipe an AI model into postTweet
Generate copy with Claude, then await postTweet(text, { publish_now: true }). You have a content pipeline in a dozen lines of Node.
Common Mistakes to Avoid
Forgetting fetch does not throw
A 401 or 429 still resolves the promise. If you skip the response.ok check, failures pass silently and your tweet never goes out. Always inspect the status.
Sending publish_now and scheduled_date together
They are mutually exclusive. Send one or the other. Include both and the request is rejected. Omit both only when you want a draft.
Using api.opentweet.io
That host does not resolve. The REST base is https://opentweet.io/api/v1. Point every request there and keep your ot_ key in the Authorization header.
Frequently Asked Questions
How do I post to Twitter from Node.js?
Send a POST request to https://opentweet.io/api/v1/posts using Node's built-in fetch. Set the Authorization header to Bearer ot_your_key and send a JSON body like {"text":"Hello","publish_now":true}. No OAuth 1.0a, no consumer keys, no Twitter developer account. Works on Node 18+ where fetch is global.
Do I need the Twitter API or OAuth to tweet from Node?
No. The X API requires a developer account and OAuth 1.0a request signing, which is painful to implement by hand in Node. OpenTweet uses a single ot_ Bearer key and plain JSON, so there is no OAuth flow, no signature, and no developer portal application to build.
Can I schedule tweets from Node.js?
Yes. Send a scheduled_date in ISO 8601 format instead of publish_now. The two fields are mutually exclusive. OpenTweet publishes the tweet at that time, so you never run your own cron or background worker. To schedule many at once, POST to /posts/batch-schedule with up to 50 schedules.
Which Node versions work with OpenTweet?
Any Node version with fetch works out of the box, meaning Node 18 and newer. On older versions you can install node-fetch or axios and call the same endpoint. There is no OpenTweet SDK to install because the API is plain REST with JSON.
Keep exploring
OpenTweet vs the X API
Post, schedule, and automate X without a developer account or the $200/mo minimum.
Post to X without an API
The clean, account-safe way to post to X from your code or an AI agent.
Twitter MCP Server
Give Claude, Cursor, and OpenClaw the ability to post to X. 36 tools included.
Developer docs
Quickstart, API keys, MCP setup, and the REST reference for posting to X from code or an agent.
XMCP vs OpenTweet
X's official MCP server bills per API call and cannot schedule. Compare it with the flat-fee hosted MCP.
MCP for AI agents
Connect your AI client to X in under two minutes, no X developer account.
Developer API and keys
REST endpoints, one bearer key, and usage tracking. Build on OpenTweet.
OpenTweet for AI agents
The posting layer for autonomous agents and automations that live on X.
Build an AI Twitter persona
Give your AI agent its own X account. Setup, cadence, and the rules that keep it safe.
Start posting from Node.js today
Get your ot_ key in 2 minutes. Post your first tweet in one fetch call.